From 51e80b41a9ba507b2e877f93ea3037e92ee3f78e Mon Sep 17 00:00:00 2001 From: Janne Suominen Date: Wed, 8 May 2019 15:37:37 +0300 Subject: [PATCH] Seed code for yarf Seed code for yarf Change-Id: I220af8eba8ef38dda2a40f72b38220c50a40a446 Signed-off-by: Janne Suominen --- LICENSE | 202 ++++++++++++++++++ README.rst | 402 +++++++++++++++++++++++++++++++++++ conf/config.ini | 43 ++++ design/architecture.dia | Bin 0 -> 3540 bytes design/architecture.jpeg | Bin 0 -> 66478 bytes required-secrets/restful.yaml | 17 ++ src/setup.py | 34 +++ src/yarf/__init__.py | 15 ++ src/yarf/apihandler.py | 32 +++ src/yarf/app.py | 154 ++++++++++++++ src/yarf/authentication/__init__.py | 15 ++ src/yarf/authentication/base_auth.py | 37 ++++ src/yarf/authentication/keystone.py | 72 +++++++ src/yarf/authentication/text_base.py | 29 +++ src/yarf/baseresource.py | 46 ++++ src/yarf/config_defaults.py | 18 ++ src/yarf/exceptions.py | 18 ++ src/yarf/handlers/__init__.py | 15 ++ src/yarf/handlers/pluginhandler.py | 173 +++++++++++++++ src/yarf/helpers.py | 27 +++ src/yarf/iniloader.py | 66 ++++++ src/yarf/restfulargs.py | 119 +++++++++++ src/yarf/restfullogger.py | 67 ++++++ src/yarf/restresource.py | 93 ++++++++ src/yarf/versionhandler.py | 38 ++++ systemd/restapi.service | 27 +++ yarf.spec | 78 +++++++ 27 files changed, 1837 insertions(+) create mode 100644 LICENSE create mode 100644 README.rst create mode 100644 conf/config.ini create mode 100644 design/architecture.dia create mode 100644 design/architecture.jpeg create mode 100644 required-secrets/restful.yaml create mode 100644 src/setup.py create mode 100644 src/yarf/__init__.py create mode 100644 src/yarf/apihandler.py create mode 100644 src/yarf/app.py create mode 100644 src/yarf/authentication/__init__.py create mode 100644 src/yarf/authentication/base_auth.py create mode 100644 src/yarf/authentication/keystone.py create mode 100644 src/yarf/authentication/text_base.py create mode 100644 src/yarf/baseresource.py create mode 100644 src/yarf/config_defaults.py create mode 100644 src/yarf/exceptions.py create mode 100644 src/yarf/handlers/__init__.py create mode 100644 src/yarf/handlers/pluginhandler.py create mode 100644 src/yarf/helpers.py create mode 100644 src/yarf/iniloader.py create mode 100644 src/yarf/restfulargs.py create mode 100644 src/yarf/restfullogger.py create mode 100644 src/yarf/restresource.py create mode 100644 src/yarf/versionhandler.py create mode 100644 systemd/restapi.service create mode 100644 yarf.spec diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..707039a --- /dev/null +++ b/README.rst @@ -0,0 +1,402 @@ +:: + + Copyright 2019 Nokia + + 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. + + +========================================= +YARF REST framework +========================================= + +:Author: Janne Suominen + +.. raw:: pdf + + PageBreak + +.. sectnum:: + +.. contents:: + +.. raw:: pdf + + PageBreak + + +Introduction +============ + +What is REST +------------ +**Representational state transfer** (REST) or **RESTful** web services is a way of providing +interoperability between computer systems on the Internet. REST-compliant Web services allow +requesting systems to access and manipulate textual representations of Web resources using a +uniform and predefined set of stateless operations. + +About this project +------------------ + +YARF --- Yet Another Restful Framework + +This project provides an implementation for a generic rest framework The +framework is based on +`Flask-RESTful `__ + +| + +*The framework provides the following:* + +* A generic rest server for whole cloud + +* A plugin based interface for creating rest apis. + +| + +*At the high level, the framework serves the following purposes*: + +* Provides a unified interface for creating rest interfaces. + +* Single point of entry with plugin based authentication for apis. + +* Define the rest response and query format. + +| + +Requirements +------------ + +* Framework shall provide easy way to integrate new REST apis via plugins + +* Framework shall be integrated to the CM and provide means to automatically configure itself + +* Framework shall listen to management address of controller on all the controllers + +* Framework shall listen to external and internal load balancer address + +* Framework shall provide means to configure SSL certification + +* Framework shall be integrated to Authentication mechanism provided by the platform + +* Framework shall provide means to validate the parameters given as part of the request + +* Framework shall return an error code in case of: + + * Internal failure; Any failure within the module is considered as internal failure + + * Authentication failure; When authentication has failed or missing credentials + + * Not found; When the object is not found + +Structure of data +================= + +The implementation of this framework promotes returning JSON format +objects with key value pairs. + +Structure of the operation +-------------------------- + +The framework supports adding function calls for any HTTP requests. The +request can either: + +* Have a request in the body of the message as JSON + +* Have a request embedded in the url + +Structure of response +--------------------- + +This framework does not enforce any special structure for the response, +but **it's strongly** encouraged to use the following formatting for the +response: + +.. code:: json + + { + code: + description: + data: + } + +Where: + +* code is the return value of the api in question. + + * 0 means no error and anything other is considered as failure + +* description is the description of the possible failure (can be left empty in case there is no failure) + +* data is the data returned by the api. The data should be in dictionary (JSON) format + +The reasoning for the quite strict guidelines is: + +* Uniqueness of the response makes it easier for the upper level to check the response + +**Note**: The framework will return HTTP status code that is not 200 in +case of: + +* 500: Internal failure (ie. Uncaught exception from the plugin) + +* 401: In case of authentication failure (if authentication is defined) + +* 404: In case the object requested is not found + +High level architecture of the restful framework +================================================ + +At the high level, there is a layer built on top of flask-restful to be +able to: + +* Isolation of the framework implementation details. + +* To be able to provide more specific implementation to fit to our needs. + +* To be able to make a single point of entry to the clusters rest api + +* Flexibility to change different parts of the implementation without affecting the users of the framework. + +* Provide unique responses to caller for easy parsing + + +.. figure:: ./design/architecture.jpeg + :alt: architecture + + architecture + +Restful framework interfaces +============================ + +RestResource +------------ + +All the plugins have to inherit from this class. This class is the basis +of the plugin framework. All the plugins that inherit from this object +and are defined in plugin specific inifile will be automatically +imported. + +The http requests will be converted to corresponding lowercase +functions. For example: request method GET will call *get()* function and +POST will call *post()*. + +The resource should also define endpoints where it want's to register +these calls. This is done by setting the *endpoint* class variable list. + + +For decorating functions with decorators *extra_wrappers* can be used. +The function must return either the function or a dictionary that is of +the same format defined in `Structure of Response`_. + +To have authentication for the module adding class variable named +*authentication_method* needs to be defined. For production environment +this should be left untouched since the authentication should be controlled +centrally by the framework. + +For logging there is a class variable called *logger* that works like a +normal logger. + +An example of a plugin can look like this: + +.. _test_rest: +.. code:: python + + class TestRest(RestResource): + endpoints = ['test'] + def get(self): + self.logger.debug("Got get request") + return {"code": 0, "description": "", "data": "Foobar"} + +Arguments: +~~~~~~~~~~ + +For parsing the arguments from the message body there function defined in `RestResource`_ +called *get_args*. This function will return the arguments that are in the request. +The parser needs to be initialized with *parser_arguments* variable that is a list of +variables your module want's to parse. + +If one needs to define more complex type of an argument it can be done with the help of *RequestArgument*. +This class provides the means of: + + * Setting a default value + + * Validation of the value by callback function + + * The type of the value + +When this type of argument is passed as one (or more) of the values. The validation +will be automatically triggered when calling the *get_args* from the *RestResource*. + + +BaseAuth +-------- + +This class defines the Base for the authentication. + +The class needs to define function *is_authenticated*. The function +gets the request as an argument. + +This function will be called when a plugin has specified the +authentication method as a derived class of BaseAuth. + +Here is an example of a very simple authentication class. + +.. code:: python + + from base_auth import BaseAuthMethod + + class TextBase(BaseAuthMethod): + def __init__(self): + super(TextBase, self).__init__() + self.user = '' + self.password = '' + with open('/tmp/foo') as f: + self.user, self.password = f.read().strip().split(':') + + def is_authenticated(self, request): + if request.authorization and request.authorization.username == self.user and request.authorization.password == self.password: + return True + return False + +Keystone +~~~~~~~~ + +For keystone additional configuration is needed: + +* User with admin role needs to be added (or admin used) + +* The config.ini has to contain the credentials and the url of keystone + +The following configuration needs to be added to config.ini + +.. code:: ini + + [keystone] + user=restful + password=foobar + auth_uri=http://192.168.1.15:5000 + +After the configuration is done and the authentication will be needed +then the http headers have to contain token with admin privileges as +X-Auth-Token. + +Restful framework binary +======================== + +The framework has only one binary. It's called restapi. The server will +be automatically started during the deployment. + +restapi config file +------------------- + +The default configuration file for the restful server is located at +/etc/yarf/config.ini + +To override the default config file, restapi can be started with command +line parameter --config. This allows testing plugins without +interference to the rest of the system. + +The config file contains the following parameters: + +.. code:: ini + + [restframe] + + #The port that the restful app will listen DEFAULT:61200 + port=61200 + #The IP address that the restful app will bind to DEFAULT:127.0.0.1 + ip_address=127.0.0.1 + + + #Use SSL or not + #If true then private key and certificate has to be also given DEFAULT:False + use_ssl=false + #ssl_private_key=PATHTOKEY/KEY.key + #ssl_certificate=PATHTOCERTIFICATE/CERT.crt + + #The directory where the handlers are + #Defaults to /opt/yarf/handlers + handler_directory=/opt/yarf/handlers + +The configuration file will be generated with an ansible module that will configure the framework. +Restapi service will run on all the controllers and listen to the controller internal management IP. +HAProxy will be configured so that clients can take a connection to the internal loadbalancer address +(or internal VIP) or external loadbalancer address (external VIP). +The framework will listen to port 61200. + +Creating plugins +================ + +First of all you need to have your own Class defined like the described +in `RestResource`_. The second thing needed is an ini file that describes +the handlers for different requests and the api version of the handler. + +The plugins should be placed in their own directory under : + +/opt/yarf/handlers. + +The thing that needs to be remembered that the object lifetime of *RestResource* is +and will be only the duration of the query. Any data stored by that query that is needed +cannot be stored in the internal variables of the module. This is anyway against the +*statelessness* nature of rest. + +Ini file for plugin +------------------- + +The recommendation is to have your own directory (although not mandatory) +per plugin. Within that directory you have to create an inifile that is +of the following format: + +.. code:: ini + + [] + handlers= + +Where the name of the inifile will the first part of your path on the +rest server. API_VERSION the second And the handler endpoint(s) the +third. + +For example if one would create an inifile for the `test_rest`_ resource +it would look like this: testing.ini: + +.. code:: ini + + [v1] + handlers=TestRest + +Then if you want to test your api you could do that with curl: + +curl http://testing/test/v1/test + +.. code:: json + + { + "code": 0, + "description": "u''" + "data": "u'Foobar'" + } + +There is also a helper to check the apis and their locations: + +curl http://testing/test/apis + +.. code:: json + + [ + { + "href": "http://testing/test/v1", + "id": "v1" + } + ] + + diff --git a/conf/config.ini b/conf/config.ini new file mode 100644 index 0000000..509e7ac --- /dev/null +++ b/conf/config.ini @@ -0,0 +1,43 @@ +# Copyright 2019 Nokia +# +# 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. +# + +[restframe] + +#The port that the restful app will listen DEFAULT:61200 +port=61200 +#The IP address that the restful app will bind to DEFAULT:127.0.0.1 +ip_address=127.0.0.1 + +# This option tells the restful app to run in debug mode or not. Defaults to True +debug=true + +#Use SSL or not +#If true then private key and certificate has to be also given DEFAULT:False +use_ssl=false +#ssl_private_key=PATHTOKEY/KEY.key +#ssl_certificate=PATHTOCERTIFICATE/CERT.crt + +#Authentication method. +auth_method=yarf.authentication.keystone.KeystoneAuth + +#The directory where the handlers are +#Defaults to /opt/yarf/handlers +#handler_directory=/opt/yarf/handlers + +[keystone] +user= +password= +auth_uri= + diff --git a/design/architecture.dia b/design/architecture.dia new file mode 100644 index 0000000000000000000000000000000000000000..5a538554c83f623fb5310290c51ea6e763e48653 GIT binary patch literal 3540 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�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 CRIT_RESP_LEN: + app.logger.debug('Response\'s data is too big, truncating!') + app.logger.debug('Response\'s truncated data: %s', response.data[:CRIT_RESP_LEN]) + else: + app.logger.debug('Response\'s data: %s', response.data) + + response.headers["Server"] = "Restapi" + + return response + +def get_username(): + try: + return auth_method.get_authentication(request)[1] + except Exception as err: # pylint: disable=broad-except + app.logger.warn("Failed to get username from request returning empty. Err: %s", str(err)) + return '' + + +def initialize(config, logger): + logger.info("Initializing...") + loglevel = logging.INFO if not config.get_debug() else logging.DEBUG + app.logger.setLevel(loglevel) + app.register_error_handler(Exception, handle_excp) + app.before_request(request_logger) + app.after_request(response_logger) + logger.error("%s", config.get_handler_dir()) + p = PluginLoader(config.get_handler_dir(), api, config.get_auth_method()) + auth_handler = p.get_auth_method() + handlers = p.get_modules() + for handler in handlers: + p.init_handler(handler) + + for handler in restlog.get_log_handlers(): + app.logger.addHandler(handler) + p.create_api_versionhandlers(handlers) + logger.info("Starting up...") + + +def get_wsgi_application(): + logger = restlog.get_logger() + config = get_config(None, logger) + initialize(config, logger) + return app + +def main(): + logger = restlog.get_logger() + config = get_config(sys.argv[1:], logger) + if not config: + raise ConfigError("Failed to read config file") + initialize(config, logger) + run_params = {} + run_params["debug"] = config.get_debug() + run_params["port"] = config.get_port() + run_params["host"] = config.get_ip() + # When this https://github.com/pallets/werkzeug/issues/954 is fixed then the error handling + # can be done in the error handler of app level + passthrough_errors = config.get_passthrough_errors() + run_params["passthrough_errors"] = passthrough_errors + run_params["threaded"] = config.is_threaded() + logger.debug("%s %s %s", run_params["debug"], run_params["port"], run_params["threaded"]) + if config.use_ssl(): + context = SSL.Context(SSL.SSLv23_METHOD) + context.use_privatekey_file(config.get_private_key()) + context.use_certificate_file(config.get_certificate()) + run_params['ssl_context'] = context + while True: + try: + app.run(**run_params) + except Exception as err: # pylint: disable=broad-except + logger.warning("Caught exception but starting again %s", err) + if passthrough_errors: + handle_excp(err) + else: + raise err + logger.warning("Die in piece %s", err) + func = request.environ.get('werkzeug.server.shutdown') + if func is None: + raise RuntimeError('Not running with the Werkzeug Server') + func() + + return 0 + +if __name__ == '__main__': + try: + sys.exit(main()) + except Exception as error:# pylint: disable=broad-except + print "Failure: %s" % error + sys.exit(255) diff --git a/src/yarf/authentication/__init__.py b/src/yarf/authentication/__init__.py new file mode 100644 index 0000000..f035b4a --- /dev/null +++ b/src/yarf/authentication/__init__.py @@ -0,0 +1,15 @@ +# Copyright 2019 Nokia +# +# 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. +# + diff --git a/src/yarf/authentication/base_auth.py b/src/yarf/authentication/base_auth.py new file mode 100644 index 0000000..e675b1e --- /dev/null +++ b/src/yarf/authentication/base_auth.py @@ -0,0 +1,37 @@ +# Copyright 2019 Nokia +# +# 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 abort, request +from functools import wraps + +def login_required(func): + @wraps(func) + def wrapper(*args, **kwargs): + auth_method = func.__self__.authentication_method + if auth_method is None: + return func(*args, **kwargs) + + if isinstance(auth_method, BaseAuthMethod) and auth_method.get_authentication(request)[0]: + return func(*args, **kwargs) + else: + abort(401) + return None + return wrapper + +class BaseAuthMethod(object): + def __init__(self): + pass + def get_authentication(self, req): + raise NotImplementedError("Function get_authentication not implemented") diff --git a/src/yarf/authentication/keystone.py b/src/yarf/authentication/keystone.py new file mode 100644 index 0000000..0d0c8fd --- /dev/null +++ b/src/yarf/authentication/keystone.py @@ -0,0 +1,72 @@ +# Copyright 2019 Nokia +# +# 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 keystoneauth1.identity import v3 +from keystoneauth1 import session +from keystoneclient.v3 import client +from keystoneclient.v3.tokens import TokenManager +from keystoneauth1.exceptions.http import Unauthorized, NotFound + +import yarf.restfullogger as logger + +from yarf.authentication.base_auth import BaseAuthMethod +from yarf.restfulargs import RestConfig + + +class KeystoneAuth(BaseAuthMethod): + def __init__(self): + super(KeystoneAuth, self).__init__() + self.logger = logger.get_logger() + config = RestConfig() + config.parse() + conf = config.get_section("keystone", format='dict') + try: + self.user = conf["user"] + self.password = conf["password"] + self.uri = conf["auth_uri"] + '/v3' + self.domain = "default" + except KeyError as error: + self.logger.error("Failed to find all the needed parameters. Authentication with Keystone not possible: {}" + .format(str(error))) + self.auth = v3.Password(auth_url=self.uri, + username=self.user, + password=self.password, + user_domain_id=self.domain) + self.sess = session.Session(auth=self.auth) + self.keystone = client.Client(session=self.sess) + self.tokenmanager = TokenManager(self.keystone) + + def get_authentication(self, req): + try: + token = req.headers.get("X-Auth-Token", type=str) + except KeyError: + self.logger.error("Failed to get the authentication token from request") + return (False, "") + + try: + tokeninfo = self.tokenmanager.validate(token) + except Unauthorized: + self.logger.error("Failed to authenticate with given credentials") + return (False, "") + except NotFound: + self.logger.error("Unauthorized token") + return (False, "") + except Exception as error: + self.logger.error("Failure: {}".format(str(error))) + return (False, "") + + if 'admin' in tokeninfo.role_names: + return (True, 'admin') + return (False, "") diff --git a/src/yarf/authentication/text_base.py b/src/yarf/authentication/text_base.py new file mode 100644 index 0000000..cf493b0 --- /dev/null +++ b/src/yarf/authentication/text_base.py @@ -0,0 +1,29 @@ +# Copyright 2019 Nokia +# +# 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 yarf.authentication.base_auth import BaseAuthMethod + +class TextBase(BaseAuthMethod): + def __init__(self): + super(TextBase, self).__init__() + self.user = '' + self.password = '' + with open('/tmp/foo') as f: + self.user, self.password = f.read().strip().split(':') + + def get_authentication(self, req): + if req.authorization and req.authorization.username == self.user and req.authorization.password == self.password: + return (True, "") + return (False, "") diff --git a/src/yarf/baseresource.py b/src/yarf/baseresource.py new file mode 100644 index 0000000..14350be --- /dev/null +++ b/src/yarf/baseresource.py @@ -0,0 +1,46 @@ +# Copyright 2019 Nokia +# +# 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_restful import Resource, reqparse +from yarf.authentication.base_auth import login_required + +class BaseResource(Resource): + # THESE VALUES ARE FILLED BY THE FRAMEWORK + method_decorators = None +# authentication_method = None + api_versions = [] + subarea = "none" + parser = None + logger = None + + # USED INTERNALLY ONLY + @classmethod + def add_wrappers(cls): + cls.method_decorators = [login_required] + for extra_wrapper in cls.extra_wrappers: + if extra_wrapper not in cls.method_decorators: + cls.logger.debug("Adding wrapper %s", extra_wrapper) + cls.method_decorators.append(extra_wrapper) + else: + cls.logger.debug("Not added %s", extra_wrapper) + + @classmethod + def add_parser_arguments(cls): + cls.parser = reqparse.RequestParser() + for argument in cls.parser_arguments: + if isinstance(argument, cls.int_arg_class): + cls.parser.add_argument(argument.argument_class) + else: + cls.parser.add_argument(argument) diff --git a/src/yarf/config_defaults.py b/src/yarf/config_defaults.py new file mode 100644 index 0000000..e6712ed --- /dev/null +++ b/src/yarf/config_defaults.py @@ -0,0 +1,18 @@ +# Copyright 2019 Nokia +# +# 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. +# + +default_config_file = "/etc/yarf/config.ini" +default_section = "restframe" +config_defaults = {"port":"61200", "ip_address": "127.0.0.1", "use_ssl": "False", "handler_directory": '/usr/lib/python2.7/site-packages/yarf/handlers/', "threaded": "True", "passthrough_errors": "True"} diff --git a/src/yarf/exceptions.py b/src/yarf/exceptions.py new file mode 100644 index 0000000..c6675a4 --- /dev/null +++ b/src/yarf/exceptions.py @@ -0,0 +1,18 @@ +# Copyright 2019 Nokia +# +# 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 ConfigError(Exception): + def __init__(self, message): + super(ConfigError, self).__init__(message) diff --git a/src/yarf/handlers/__init__.py b/src/yarf/handlers/__init__.py new file mode 100644 index 0000000..f035b4a --- /dev/null +++ b/src/yarf/handlers/__init__.py @@ -0,0 +1,15 @@ +# Copyright 2019 Nokia +# +# 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. +# + diff --git a/src/yarf/handlers/pluginhandler.py b/src/yarf/handlers/pluginhandler.py new file mode 100644 index 0000000..aa145a6 --- /dev/null +++ b/src/yarf/handlers/pluginhandler.py @@ -0,0 +1,173 @@ +# Copyright 2019 Nokia +# +# 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 os.path +import sys +import inspect +from yarf.iniloader import INILoader +from yarf.restresource import RestResource +from yarf.versionhandler import VersionHandler +from yarf.authentication.base_auth import BaseAuthMethod +from yarf.exceptions import ConfigError +import yarf.restfullogger as restlog + +class PluginLoader(object): + def __init__(self, path, api, auth_method): + self.logger = restlog.get_logger() + self.plugin_class_type = RestResource + self.auth_method = self._get_auth_method(auth_method) + self.path = path + self.api = api + + def get_module_dirs(self): + files = os.listdir(self.path) + modules = [] + for f in files: + if os.path.isdir("%s/%s"%(self.path, f)): + modules.append("%s/%s"%(self.path, f)) + return modules + + def _get_auth_method(self, authmethod): + auth_class_module = None + class_name = None + try: + auth_class_module, class_name = authmethod.rsplit('.', 1) + except ValueError: + error = "Cannot decode the authentication method from configuration file" + self.logger.error(error) + raise ConfigError(error) + auth_classes = self._get_classes_wanted_classes(auth_class_module, [class_name], BaseAuthMethod) + if auth_classes is None or auth_classes == []: + error = "Cannot find the authentication class in provided module %s %s" % (auth_class_module, class_name) + raise ConfigError(error) + return auth_classes[0] + + def _get_classes_wanted_classes(self, module_name, wanted_modules, class_type): + classes = [] + try: + __import__(module_name) + except ImportError: + self.logger.error("Failed import in %s, skipping", module_name) + return None + module = sys.modules[module_name] + for obj_name in dir(module): + # Skip objects that are meant to be private. + if obj_name.startswith('_'): + continue + # Skip the same name that base class has + elif obj_name == class_type.__name__: + continue + elif obj_name not in wanted_modules: + continue + itm = getattr(module, obj_name) + if inspect.isclass(itm) and issubclass(itm, class_type): + classes.append(itm) + return classes + + def get_classes_from_dir(self, directory, wanted_modules): + classes = [] + if directory not in sys.path: + sys.path.append(directory) + for fname in os.listdir(directory): + root, ext = os.path.splitext(fname) + if ext != '.py' or root == '__init__': + continue + module_name = "%s" % (root) + + mod_classes = self._get_classes_wanted_classes(module_name, wanted_modules, self.plugin_class_type) + if mod_classes: + classes.extend(mod_classes) + return classes + + def get_modules_from_dir(self, module_dir): + modules = {} + for f in os.listdir(module_dir): + if not f.endswith(".ini"): + continue + root, _ = os.path.splitext(f) + loader = INILoader("%s/%s" %(module_dir, f)) + sections = loader.get_sections() + modules[root] = {} + for section in sections: + handlers = loader.get_handlers(section) + if handlers: + modules[root][section] = handlers + else: + self.logger.error("Problem in the configuration file %s in section %s: No handlers found", f, section) + return modules + + def get_auth_method(self): + return self.auth_method() + + def get_modules(self): + dirs = self.get_module_dirs() + auth_class = self.auth_method() + modules = [] + for d in dirs: + wanted_modules = self.get_modules_from_dir(d) + for mod in wanted_modules.keys(): + for api_version in wanted_modules[mod].keys(): + classes = self.get_classes_from_dir(d, wanted_modules[mod][api_version]) + if not classes: + continue + for c in classes: + setattr(c, "subarea", mod) + if getattr(c, "authentication_method", "EMPTY") == "EMPTY": + setattr(c, "authentication_method", auth_class) + if getattr(c, "api_versions", None): + c.api_versions.append(api_version) + else: + setattr(c, "api_versions", [api_version]) + for cls in classes: + if cls not in modules: + modules.append(cls) + return modules + + def create_endpoints(self, handler): + endpoint_list = [] + for endpoint in handler.endpoints: + for api_version in handler.api_versions: + self.logger.debug("Registering /%s/%s/%s for %s", handler.subarea, api_version, endpoint, handler.__name__) + endpoint_list.append("/%s/%s/%s"% (handler.subarea, api_version, endpoint)) + self.api.add_resource(handler, *(endpoint_list)) + + def add_logger(self, handler): + self.logger.info("Adding logger to: %s", handler.__name__) + handler.logger = self.logger + + def init_handler(self, handler): + + self.add_logger(handler) + handler.add_wrappers() + self.create_endpoints(handler) + handler.add_parser_arguments() + + def create_api_versionhandlers(self, handlers): + apiversions = {} + endpoint_list = [] + for handler in handlers: + subarea = handler.subarea + if apiversions.get(subarea, False): + for hapiversion in handler.api_versions: + if hapiversion not in apiversions[subarea]: + apiversions[subarea].append(hapiversion) + else: + apiversions[subarea] = handler.api_versions + self.logger.debug("Registering /%s/apis for %s", subarea, subarea) + endpoint_list.append("/%s/apis" % subarea) + setattr(VersionHandler, "versions", apiversions) + setattr(VersionHandler, "method_decorators", []) + self.api.add_resource(VersionHandler, *(endpoint_list)) diff --git a/src/yarf/helpers.py b/src/yarf/helpers.py new file mode 100644 index 0000000..917a506 --- /dev/null +++ b/src/yarf/helpers.py @@ -0,0 +1,27 @@ +# Copyright 2019 Nokia +# +# 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 re + +SECRETS = ["password", "community-string"] + + +def remove_secrets(endpoint): + for secret in SECRETS: + endpoint = re.sub(r'%s=(.[^&]*)' % secret, "%s=*****" % secret, endpoint) + return endpoint + + + diff --git a/src/yarf/iniloader.py b/src/yarf/iniloader.py new file mode 100644 index 0000000..3c762ea --- /dev/null +++ b/src/yarf/iniloader.py @@ -0,0 +1,66 @@ +# Copyright 2019 Nokia +# +# 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 configparser +from yarf.exceptions import ConfigError + + +class INILoader(dict): + def __init__(self, inifile, defaults=None, defaultsection=None): + super(INILoader, self).__init__(self) + self.inifile = inifile + self.handlers = 'handlers' + self.configparser = configparser.ConfigParser(defaults) + self.config = self.configparser.read(inifile) + self.defaultsection = defaultsection + if inifile not in self.config: + raise ConfigError("Failed to read config file: %s" % inifile) + + def get_sections(self): + return self.configparser.sections() + + def get_handlers(self, section): + return self[section][self.handlers].split(',') + + def __getitem__(self, key): + try: + return self.configparser[key] + except KeyError: + raise ConfigError("No such key %s" % key) + + def get(self, key, section=None, type_of_value=str): + if section is None and self.defaultsection is not None: + section = self.defaultsection + else: + return None + + if type_of_value is int: + return self.configparser.getint(section, key) + elif type_of_value is bool: + return self.configparser.getboolean(section, key) + elif type_of_value is float: + return self.configparser.getfloat(section, key) + return self.configparser.get(section, key) + + def keys(self): + return self.configparser.sections() + + def get_section(self, section, format='list'): + if section in self.keys(): + items = self.configparser.items(section) + if format == 'dict': + return dict(items) + return items + return None diff --git a/src/yarf/restfulargs.py b/src/yarf/restfulargs.py new file mode 100644 index 0000000..5624b5c --- /dev/null +++ b/src/yarf/restfulargs.py @@ -0,0 +1,119 @@ +# Copyright 2019 Nokia +# +# 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 argparse +import sys + +import yarf.config_defaults as config_defaults +import yarf.restfullogger as restfullogger + +from yarf.iniloader import INILoader +from yarf.exceptions import ConfigError + + +def exception_handler(func): + def exception_wrapper(*args, **kwargs): + try: + restlogger = restfullogger.get_logger() + restlogger.debug("calling {}".format(func.__name__)) + ret = func(*args, **kwargs) + return ret + except Exception as error: + restlogger.info("Exception from function {} (error: {})".format(func.__name__, str(error))) + if isinstance(error, ConfigError): + raise error + else: + raise ConfigError(str(error)) + return exception_wrapper + + +class RestConfig(object): + __restinstance = None + + def __new__(cls): + if RestConfig.__restinstance is None: + RestConfig.__restinstance = object.__new__(cls) + return RestConfig.__restinstance + + def __init__(self): + self.default_section = config_defaults.default_section + self.config_default = config_defaults.config_defaults + self.default_config_file = config_defaults.default_config_file + + self.config = None + self.config_file = None + + @exception_handler + def parse(self, args=sys.argv[1:]): + parser = argparse.ArgumentParser(description='Restful server') + parser.add_argument('--config', + type=str, + default=self.default_config_file, + help="Configuration file", + dest='config_file') + + args = parser.parse_args(args) + self.config_file = args.config_file + self.config = INILoader(self.config_file, defaults=self.config_default, defaultsection=self.default_section) + + @exception_handler + def get_port(self): + return self.config.get('port', type_of_value=int) + + @exception_handler + def get_ip(self): + return self.config.get('ip_address') + + @exception_handler + def use_ssl(self): + return self.config.get('use_ssl', type_of_value=bool) + + @exception_handler + def get_private_key(self): + if self.use_ssl(): + return self.config.get('ssl_private_key') + return None + + @exception_handler + def get_certificate(self): + if self.use_ssl(): + return self.config.get('ssl_certificate') + return None + + @exception_handler + def get_handler_dir(self): + return self.config.get('handler_directory') + + def get_section(self, section, format='list'): + return self.config.get_section(section, format=format) + + def get_debug(self): + return self.config.get('debug', type_of_value=bool) + + @exception_handler + def get_passthrough_errors(self): + return self.config.get('passthrough_errors', type_of_value=bool) + + @exception_handler + def is_threaded(self): + return self.config.get('threaded', type_of_value=bool) + + @exception_handler + def get_auth_method(self): + return self.config.get('auth_method') + + +def get_config(): + return RestConfig() diff --git a/src/yarf/restfullogger.py b/src/yarf/restfullogger.py new file mode 100644 index 0000000..44fb074 --- /dev/null +++ b/src/yarf/restfullogger.py @@ -0,0 +1,67 @@ +# Copyright 2019 Nokia +# +# 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 logging +import logging.handlers +restlogger = None + + +class RestfulLogger(object): + def __init__(self): + self.logger = logging.getLogger("Restfulserver") + self.logger.setLevel(logging.DEBUG) + # werkzug logs out endpoint in DEBUG level, and aaa feature endpoint contains password + # in clear text, so log level setting is needed to avoid showing password in journalctl + logging.getLogger('werkzeug').setLevel(logging.WARNING) + self.handlers = [] + self.sysloghandler = self._get_syslog_handler() + self.handlers.append(self.sysloghandler) + self._add_handlers() + + def __del__(self): + handlers = self.logger.handlers[:] + for handler in handlers: + handler.close() + self.logger.removeHandler(handler) + + @staticmethod + def _get_syslog_handler(): + sh = logging.handlers.SysLogHandler(address='/dev/log') + formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') + sh.setFormatter(formatter) + sh.setLevel(logging.NOTSET) + return sh + + def _add_handlers(self): + for handler in self.handlers: + self.logger.addHandler(handler) + + def get_handlers(self): + return self.handlers + + def get_logger(self): + return self.logger + +def get_logger(): + global restlogger + if not restlogger: + restlogger = RestfulLogger() + return restlogger.get_logger() + +def get_log_handlers(): + global restlogger + if not restlogger: + restlogger = RestfulLogger() + return restlogger.get_handlers() diff --git a/src/yarf/restresource.py b/src/yarf/restresource.py new file mode 100644 index 0000000..4e8af83 --- /dev/null +++ b/src/yarf/restresource.py @@ -0,0 +1,93 @@ +# Copyright 2019 Nokia +# +# 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 inspect +import six +from flask import request +from flask_restful import reqparse +from werkzeug.exceptions import BadRequest + +from yarf.baseresource import BaseResource + +class RequestArgument(object): + """ More advanced arguments + Parameters: + name: Name of the argument + default: The default value if not defined + validate: function pointer to a validation function that will + be called to validate the argument. + The function should return tuple containing + status: (boolean) True if validation passed + False if not + reason: (string) Setting the reasoning for the + failure + typeof: The typeof the argument. The argument will be converted + to the type you define. Should be a function pointer + """ + def __init__(self, name, default=None, validate=None, typeof=lambda x: six.text_type(x)): + self.argument_class = reqparse.Argument(name=name, default=default, type=typeof) + if validate and inspect.isfunction(validate): + self.validate_func = validate + else: + self.validate_func = None + self.name = name + + def validate(self, value): + if not self.validate_func: + return + status, reason = self.validate_func(value) + if not status: + raise BadRequest(description=reason) + +class RestResource(BaseResource): + """ Class from which the plugins should inherit + Variables: + extra_wrappers: are function wrappers that will + be executed when any function is + executed by the frame (for example + get) + parser_arguments: Are the arguments that can be defined + if you need arguments for your plugin + these arguments can be fetched with + get_args + """ + extra_wrappers = [] + parser_arguments = [] + endpoints = None + int_arg_class = RequestArgument + + """ Function to get arguments from request + The function will call validate to the + arguments if they are of type RequestArgument + Returns: A dictionary of the arguments + """ + @classmethod + def get_args(cls): + args = cls.parser.parse_args() + for arg in args.keys(): + for parg in cls.parser_arguments: + if isinstance(parg, cls.int_arg_class) and parg.name == arg: + parg.validate(args[arg]) + return args + + @classmethod + def get_token(cls): + token = "" + try: + token = request.headers.get("X-Auth-Token", type=str) + except KeyError as err: + cls.logger.info("Failed to get auth token from request.") + return token + diff --git a/src/yarf/versionhandler.py b/src/yarf/versionhandler.py new file mode 100644 index 0000000..5937817 --- /dev/null +++ b/src/yarf/versionhandler.py @@ -0,0 +1,38 @@ +# Copyright 2019 Nokia +# +# 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 yarf.restresource import RestResource + +def get_api(req): + splitted = req.full_path.split("/") + domain = splitted[1] + return domain + +class VersionHandler(RestResource): + versions = {} + + def create_api_reference(self, api): + ref = [] + for version in self.versions[api]: + ver = {} + ver['id'] = version + ver['href'] = "%s%s/%s" %(request.host_url, api, version) + ref.append(ver) + return ref + + def get(self): + api = get_api(request) + return self.create_api_reference(api) diff --git a/systemd/restapi.service b/systemd/restapi.service new file mode 100644 index 0000000..f3156d2 --- /dev/null +++ b/systemd/restapi.service @@ -0,0 +1,27 @@ +# Copyright 2019 Nokia +# +# 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. +# + +[Unit] +Description=Restful api server +DefaultDependencies=no + +[Service] +Restart=on-failure +RestartSec=3 +ExecStart=/usr/local/bin/restapi +User=restapi + +[Install] +WantedBy=multi-user.target diff --git a/yarf.spec b/yarf.spec new file mode 100644 index 0000000..70dd0c8 --- /dev/null +++ b/yarf.spec @@ -0,0 +1,78 @@ +# Copyright 2019 Nokia +# +# 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. +# + +Name: yarf +Version: %{_version} +Release: 1%{?dist} +Summary: Yet Another Restfulframework +License: %{_platform_licence} +Source0: %{name}-%{version}.tar.gz +Vendor: %{_platform_vendor} + +Requires: python-flask, python2-flask-restful, python2-configparser, python2-requests, mod_wsgi, python2-six +BuildRequires: python +BuildRequires: python-setuptools + +%description +Yet Another Restfulframework. + +%prep +#./autogen.sh +%autosetup + +%build + +%install +rm -rf $RPM_BUILD_ROOT +mkdir -p %{buildroot}%{_platform_etc_path}/yarf +mkdir -p %{buildroot}%{_platform_etc_path}/required-secrets +mkdir -p %{buildroot}%{_unitdir}/ +mkdir -p %{buildroot}/var/log/restapi +cp required-secrets/*.yaml %{buildroot}/%{_platform_etc_path}/required-secrets +#mkdir -p {buildroot}/etc/httpd/conf.d/ +#mkdir -p {buildroot}/var/www/yarf/ + +cd src && python setup.py install --root %{buildroot} --no-compile --install-purelib %{_python_site_packages_path} --install-scripts %{_platform_bin_path} && cd - + +rsync -rv systemd/* %{buildroot}%{_unitdir}/ + +%files +%defattr(0755,root,root,0755) +%{_python_site_packages_path}/yarf* +%attr(0755,restapi, restapi) %{_platform_etc_path}/yarf/ +%{_platform_etc_path}/required-secrets/restful.yaml +#/etc/ansible/roles/restful +#/opt/openstack-ansible/playbooks/yarf.yml +%attr(0755,root, root) %{_platform_bin_path}/restapi +# %attr(0644,root, root) %{_unitdir}/restapi.service +%attr(0644,root, root) %{_unitdir}/* +%dir %attr(0770, restapi,restapi) /var/log/restapi + +%pre +/usr/bin/getent passwd restapi > /dev/null||/usr/sbin/useradd -r -s /sbin/nologin -M restapi + +%post +if [ $1 -eq 2 ]; then + if [ -f %{_platform_etc_path}/restful/config.ini ]; then + sudo /usr/bin/systemctl restart restapi + fi +fi + +%postun + +#Uninstall +if [ $1 -eq 0 ];then + /usr/sbin/userdel restapi +fi -- 2.16.6