diff options
author | stark, steven <steven.stark@att.com> | 2020-03-11 14:30:58 -0700 |
---|---|---|
committer | stark, steven <steven.stark@att.com> | 2020-03-12 14:41:09 -0700 |
commit | c02d3ed39e53d4ce56406a43089fc4a336c43f17 (patch) | |
tree | 8354fe71b2b4d379f36042b76c0e29775692f03e /onap-client/onap_client/client | |
parent | d7d8722ce27e308defb6764d8d76f85ce7d63927 (diff) |
[VVP] Adding onap-client intial commit.
This is supplementary tooling to interact with various ONAP applications.
It will be used by the OVP VNF Testcase, it's not deployed
with the ONAP platform.
Issue-ID: VVP-381
Change-Id: I2ff3952ba8f4b9448acb5a24717ccc3b1f0a92fe
Signed-off-by: stark, steven <steven.stark@att.com>
Diffstat (limited to 'onap-client/onap_client/client')
-rw-r--r-- | onap-client/onap_client/client/__init__.py | 36 | ||||
-rw-r--r-- | onap-client/onap_client/client/catalog.py | 156 | ||||
-rw-r--r-- | onap-client/onap_client/client/clients.py | 85 | ||||
-rw-r--r-- | onap-client/onap_client/client/request.py | 203 | ||||
-rw-r--r-- | onap-client/onap_client/client/response.py | 115 |
5 files changed, 595 insertions, 0 deletions
diff --git a/onap-client/onap_client/client/__init__.py b/onap-client/onap_client/client/__init__.py new file mode 100644 index 0000000..5519a84 --- /dev/null +++ b/onap-client/onap_client/client/__init__.py @@ -0,0 +1,36 @@ +# -*- coding: utf8 -*- +# ============LICENSE_START======================================================= +# org.onap.vvp/validation-scripts +# =================================================================== +# Copyright © 2020 AT&T Intellectual Property. All rights reserved. +# =================================================================== +# +# Unless otherwise specified, all software contained herein is licensed +# under the Apache License, Version 2.0 (the "License"); +# you may not use this software 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. +# +# +# +# Unless otherwise specified, all documentation contained herein is licensed +# under the Creative Commons License, Attribution 4.0 Intl. (the "License"); +# you may not use this documentation except in compliance with the License. +# You may obtain a copy of the License at +# +# https://creativecommons.org/licenses/by/4.0/ +# +# Unless required by applicable law or agreed to in writing, documentation +# 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. +# +# ============LICENSE_END============================================ diff --git a/onap-client/onap_client/client/catalog.py b/onap-client/onap_client/client/catalog.py new file mode 100644 index 0000000..419d00f --- /dev/null +++ b/onap-client/onap_client/client/catalog.py @@ -0,0 +1,156 @@ +# -*- coding: utf8 -*- +# ============LICENSE_START======================================================= +# org.onap.vvp/validation-scripts +# =================================================================== +# Copyright © 2020 AT&T Intellectual Property. All rights reserved. +# =================================================================== +# +# Unless otherwise specified, all software contained herein is licensed +# under the Apache License, Version 2.0 (the "License"); +# you may not use this software 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. +# +# +# +# Unless otherwise specified, all documentation contained herein is licensed +# under the Creative Commons License, Attribution 4.0 Intl. (the "License"); +# you may not use this documentation except in compliance with the License. +# You may obtain a copy of the License at +# +# https://creativecommons.org/licenses/by/4.0/ +# +# Unless required by applicable law or agreed to in writing, documentation +# 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. +# +# ============LICENSE_END============================================ + +from abc import ABC, abstractmethod +from onap_client.lib import make_request + + +class Catalog(ABC): + """Abstract class for an ONAP client, automatically loads + child classes as attributes.""" + + class CallHandle: + """Attached as an attribute for each catalog entry in a catalog. + Used to make a request to ONAP.""" + + def __init__(self, catalog_resource): + self.resource = catalog_resource + + def __call__(self, **kwargs): + return make_request(self.resource, **kwargs) + + def __init__(self): + """Iterates through all child classes and attaches them as attributes, named + after the namespace property. + + If the child Catalog class has items in the + catalog_resources property, they will be added as attributes to the child attribute + as a CallHandle object. + """ + self.catalog_items = {} + + for cls in self.__class__.__subclasses__(): + subclass = cls() + namespace = subclass.namespace + catalog_resources = subclass.catalog_resources + + for k, v in catalog_resources.items(): + subclass.load(k, v) + + setattr(self, namespace, subclass) + + def load(self, item_name, resource_data): + """Consume a catalog resource entry as an APICatalogResource, + and set it as an attribute on this.class as a CallHandle object""" + resource = APICatalogResource(item_name, resource_data) + + self.catalog_items[item_name] = resource + setattr(self, item_name.lower(), self.CallHandle(resource)) + + @property + @abstractmethod + def namespace(self): + raise NotImplementedError + + @property + @abstractmethod + def catalog_resources(self): + raise NotImplementedError + + +class APICatalogResource: + """Class representation of a single catalog entry""" + + def __init__(self, catalog_resource_name, resource_data): + """ + :catalog_resource_name: name of the catalog resource + :resource_data: dictionary containing catalog resource attributes + """ + self.catalog_resource_name = catalog_resource_name + self.catalog_resource_data = resource_data + + @property + def verb(self): + return self.catalog_resource_data.get("verb", None) + + @property + def description(self): + return self.catalog_resource_data.get("description", None) + + @property + def uri(self): + return self.catalog_resource_data.get("uri", None) + + @property + def payload(self): + return self.catalog_resource_data.get("payload", None) + + @property + def uri_parameters(self): + return self.catalog_resource_data.get("uri-parameters", []) + + @property + def payload_parameters(self): + return self.catalog_resource_data.get("payload-parameters", []) + + @property + def payload_path(self): + return self.catalog_resource_data.get("payload-path", []) + + @property + def file_parameters(self): + return self.catalog_resource_data.get("files-parameters", []) + + @property + def header_parameters(self): + return self.catalog_resource_data.get("header_parameters", []) + + @property + def success_code(self): + return self.catalog_resource_data.get("success_code", None) + + @property + def headers(self): + return self.catalog_resource_data.get("headers", None) + + @property + def return_data(self): + return self.catalog_resource_data.get("return_data", {}) + + @property + def auth(self): + return self.catalog_resource_data.get("auth", None) diff --git a/onap-client/onap_client/client/clients.py b/onap-client/onap_client/client/clients.py new file mode 100644 index 0000000..0c4605f --- /dev/null +++ b/onap-client/onap_client/client/clients.py @@ -0,0 +1,85 @@ +# -*- coding: utf8 -*- +# ============LICENSE_START======================================================= +# org.onap.vvp/validation-scripts +# =================================================================== +# Copyright © 2020 AT&T Intellectual Property. All rights reserved. +# =================================================================== +# +# Unless otherwise specified, all software contained herein is licensed +# under the Apache License, Version 2.0 (the "License"); +# you may not use this software 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. +# +# +# +# Unless otherwise specified, all documentation contained herein is licensed +# under the Creative Commons License, Attribution 4.0 Intl. (the "License"); +# you may not use this documentation except in compliance with the License. +# You may obtain a copy of the License at +# +# https://creativecommons.org/licenses/by/4.0/ +# +# Unless required by applicable law or agreed to in writing, documentation +# 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. +# +# ============LICENSE_END============================================ + +import importlib +import onap_client +import pkgutil +import inspect + +from onap_client.client.catalog import Catalog + + +class Client(Catalog): + """Base class for the ONAP client. Subclasses are dynamically + loaded and added as attributes. Instantiate and use this class + to interact with ONAP.""" + def __init__(self): + self.modules = import_submodules(onap_client) + super().__init__() + + @property + def namespace(self): + return "onap" + + @property + def catalog_resources(self): + return {} + + @property + def utility_functions(self): + utility_functions = {} + for module_name, module in self.modules.items(): + all_functions = inspect.getmembers(module, inspect.isfunction) + for func in all_functions: + function = func[1] + if hasattr(function, "utility_function"): + utility_functions[func[0]] = func[1] + return utility_functions + + +def import_submodules(package, recursive=True): + """Import all the modules in onap-client, except for those starting + with tests*. This is needed so that the Client object can register child classes""" + if isinstance(package, str): + package = importlib.import_module(package) + results = {} + for loader, name, is_pkg in pkgutil.walk_packages(package.__path__): + full_name = package.__name__ + "." + name + results[full_name] = importlib.import_module(full_name) + if recursive and is_pkg and full_name.find("tests") == -1: + results.update(import_submodules(full_name)) + return results diff --git a/onap-client/onap_client/client/request.py b/onap-client/onap_client/client/request.py new file mode 100644 index 0000000..0e40591 --- /dev/null +++ b/onap-client/onap_client/client/request.py @@ -0,0 +1,203 @@ +# -*- coding: utf8 -*- +# ============LICENSE_START======================================================= +# org.onap.vvp/validation-scripts +# =================================================================== +# Copyright © 2020 AT&T Intellectual Property. All rights reserved. +# =================================================================== +# +# Unless otherwise specified, all software contained herein is licensed +# under the Apache License, Version 2.0 (the "License"); +# you may not use this software 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. +# +# +# +# Unless otherwise specified, all documentation contained herein is licensed +# under the Creative Commons License, Attribution 4.0 Intl. (the "License"); +# you may not use this documentation except in compliance with the License. +# You may obtain a copy of the License at +# +# https://creativecommons.org/licenses/by/4.0/ +# +# Unless required by applicable law or agreed to in writing, documentation +# 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. +# +# ============LICENSE_END============================================ + +import jinja2 +import requests +import json +import os +import copy + +from onap_client.client.response import ResponseHandler +from onap_client.config import LOG as logger +from onap_client.exceptions import FilesRequestFailure +from jinja2 import exceptions as jinja_exceptions + + +class RequestHandler: + """Handles a APICatalogRequestObject to make a request + and returns a ResponseHandler object""" + + def __init__(self, request_object): + """ + :request_object: APICatalogRequestObject + """ + self.request_object = request_object + + def make_request(self): + r = Request(self.request_object) + + logger.warning("Submitting request: {}".format(self.request_object.description)) + # TODO + # Add verify to config file + return ResponseHandler(r.request(verify=False), self.request_object) + + +class Request: + """Parses a APICatalogRequestObject to fill out the + kwargs to send to the requests library""" + + def __init__(self, request_object): + """ + :request_object: APICatalogRequestObject + """ + self.request_object = request_object + self.kwargs = {} + self.response = None + + self.build_request() + + def build_request(self): + request_object = self.request_object + + if request_object.verb: + self.kwargs["method"] = request_object.verb + + if request_object.auth: + self.kwargs["auth"] = request_object.auth + + if request_object.uri: + self.kwargs["url"] = request_object.uri + + if request_object.headers: + self.kwargs["headers"] = request_object.headers + + if request_object.payload: + self.kwargs["data"] = request_object.payload + + if request_object.files: + self.kwargs["files"] = request_object.files + + debug_request = copy.deepcopy(self.kwargs) + if "auth" in debug_request: + debug_request["auth"] = "***********" + + try: + logger.info(json.dumps(debug_request, indent=4)) + except TypeError: + logger.info(debug_request) + + def request(self, verify=True): + return requests.request(**self.kwargs, verify=verify) + + +class APICatalogRequestObject: + """Fills a APICatalogResource object with request-specific data""" + + def __init__(self, api_catalog_resource, **kwargs): + """ + :api_catalog_resource: APICatalogResource object + :kwargs: key/value to fill in APICatalogResource parameters + """ + self.api_catalog_resource = api_catalog_resource + self.payload_parameters = kwargs.get("payload_parameters", {}) + self.uri_parameters = kwargs.get("uri_parameters", {}) + self.header_parameters = kwargs.get("header_parameters", {}) + self.file_parameters = kwargs.get("file_parameters", {}) + if api_catalog_resource.payload_path: + self.payload_path = kwargs.get("payload_path", {}).get( + api_catalog_resource.payload_path[0] + ) + + self.uri = "" + self.files = None + self.payload = None + self.verb = api_catalog_resource.verb + self.headers = api_catalog_resource.headers + self.success_code = api_catalog_resource.success_code + self.return_data = api_catalog_resource.return_data + self.auth = api_catalog_resource.auth + self.description = api_catalog_resource.description + + if api_catalog_resource.payload or api_catalog_resource.payload_path: + self.resolve_payload() + + if api_catalog_resource.file_parameters: + self.resolve_files() + + if isinstance(self.headers, dict): + for k, v in self.header_parameters.items(): + self.headers[k] = v + + self.resolve_uri() + + def resolve_files(self): + # TODO + # is there a better way to figure out waht params are needed? + # right now its hardcoded + file_type = self.file_parameters.get("file_type", "application/zip") + file_path = self.file_parameters.get("file_path") + if not file_path: + raise FilesRequestFailure("File path was not provided") + + try: + with open(file_path, "rb") as f: + data = f.read() + except IOError: + logger.error("file {} was not found".format(file_path)) + raise + + file_name = os.path.basename(file_path) + + self.files = {"upload": [file_name, data, file_type]} + + def resolve_payload(self): + try: + if self.api_catalog_resource.payload_path: + with open(self.payload_path, "r") as f: + self.payload = f.read() + else: + with open(self.api_catalog_resource.payload, "r") as f: + self.payload = jinja2.Template(f.read()).render( + **self.payload_parameters + ) + except jinja_exceptions.TemplateNotFound: + logger.error( + "{} file not found. Check payloads directory.".format(self.payload) + ) + raise + except FileNotFoundError: + logger.error( + "{} file not found. Check payloads directory.".format(self.payload) + ) + raise + + def resolve_uri(self): + try: + self.uri = self.api_catalog_resource.uri(**self.uri_parameters) + except KeyError: + logger.error("invalid uri keys {}.".format(self.uri_parameters)) + raise diff --git a/onap-client/onap_client/client/response.py b/onap-client/onap_client/client/response.py new file mode 100644 index 0000000..ef0aab5 --- /dev/null +++ b/onap-client/onap_client/client/response.py @@ -0,0 +1,115 @@ +# -*- coding: utf8 -*- +# ============LICENSE_START======================================================= +# org.onap.vvp/validation-scripts +# =================================================================== +# Copyright © 2020 AT&T Intellectual Property. All rights reserved. +# =================================================================== +# +# Unless otherwise specified, all software contained herein is licensed +# under the Apache License, Version 2.0 (the "License"); +# you may not use this software 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. +# +# +# +# Unless otherwise specified, all documentation contained herein is licensed +# under the Creative Commons License, Attribution 4.0 Intl. (the "License"); +# you may not use this documentation except in compliance with the License. +# You may obtain a copy of the License at +# +# https://creativecommons.org/licenses/by/4.0/ +# +# Unless required by applicable law or agreed to in writing, documentation +# 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. +# +# ============LICENSE_END============================================ + +import simplejson + +from onap_client.config import LOG as logger + + +class ResponseHandler: + """Handles a response from the requests library, + and compares it to the APICatalogRequestObject that was used to make the request. + If the request object has return_data, then it will parse the response + object and add the return data as an attribute.""" + + def __init__(self, response, request_object): + """ + :response: requests.response + :request_object: APICatalogRequestObject + """ + self.response = response + self.request_object = request_object + self.response_data = {} + self.status_code = None + self.success = False + + self.validate_response() + + def validate_response(self): + response = self.response + if self.request_object.success_code != response.status_code: + response_data = response.text + logger.error( + "Request failed with code {} and data {}".format( + response.status_code, response_data + ) + ) + else: + logger.info("Request was successful") + self.success = True + try: + response_data = response.json() + for ( + response_key, + response_items, + ) in self.request_object.return_data.items(): + response_value = response_iterator(response_data, *response_items) + if not response_value: + logger.warning( + "Request was successful but value for {} was not present in response".format( + response_key + ) + ) + setattr(self, response_key, response_value) + except simplejson.errors.JSONDecodeError: + response_data = response.text + + logger.debug("{}\n".format(response_data)) + + self.response_data = response_data + self.status_code = response.status_code + + +def response_iterator(response_content, *keys): + """helper function to search a response for return_data keys""" + props = list(keys) + + key = props.pop(0) + prop = response_content.get(key, None) + + if isinstance(prop, str) or len(props) <= 0: + return prop + elif isinstance(prop, list): + if isinstance(key, int): + return response_iterator(prop[key], *props) + else: + for x in prop: + return response_iterator(x, *props) + elif isinstance(prop, dict): + return response_iterator(prop, *props) + else: + return None |