From d19abe35942f50ef9b1a44e144c89c342145d031 Mon Sep 17 00:00:00 2001 From: Andy Ning Date: Wed, 6 Mar 2019 14:06:13 -0500 Subject: [PATCH] Keystone DB sync - introduce DB sync service This update introduces the DB record synchronization service. This new service provides REST APIs to read/write/update identity database. The REST APIs is intended to be used internally by DC Orchestrator to synchronize identity resources between central cloud and subclouds. This update also contains python client that wraps up the REST APIs into functions. The client is to be used by DC Orchestrator. This service supports the synchronization of the following identity resources: - users (local users only) - user passwords - projects - roles - project role assignments - token revocation events Story: 2002842 Task: 22787 Change-Id: Idb4aff5bac006fdd904b58c0c8b3d6a0916fbd4b Signed-off-by: Andy Ning (cherry picked from commit 6cdd47b836b3618d2ec549fe0bab273bd23ce942) --- dcdbsync/__init__.py | 23 + dcdbsync/api/README.rst | 27 + dcdbsync/api/__init__.py | 0 dcdbsync/api/api_config.py | 109 ++++ dcdbsync/api/app.py | 95 ++++ dcdbsync/api/controllers/README.rst | 11 + dcdbsync/api/controllers/__init__.py | 0 dcdbsync/api/controllers/restcomm.py | 46 ++ dcdbsync/api/controllers/root.py | 62 ++ dcdbsync/api/controllers/v1/__init__.py | 0 .../api/controllers/v1/identity/__init__.py | 0 .../api/controllers/v1/identity/identity.py | 133 +++++ .../api/controllers/v1/identity/project.py | 134 +++++ dcdbsync/api/controllers/v1/identity/role.py | 135 +++++ dcdbsync/api/controllers/v1/identity/root.py | 63 ++ .../v1/identity/token_revoke_event.py | 247 ++++++++ dcdbsync/api/controllers/v1/root.py | 60 ++ dcdbsync/api/enforcer.py | 76 +++ dcdbsync/cmd/README.rst | 9 + dcdbsync/cmd/__init__.py | 0 dcdbsync/cmd/api.py | 75 +++ dcdbsync/common/__init__.py | 0 dcdbsync/common/config.py | 124 ++++ dcdbsync/common/context.py | 146 +++++ dcdbsync/common/exceptions.py | 93 +++ dcdbsync/common/i18n.py | 25 + dcdbsync/common/messaging.py | 116 ++++ dcdbsync/common/policy.py | 54 ++ dcdbsync/common/version.py | 46 ++ dcdbsync/config-generator.conf | 14 + dcdbsync/db/__init__.py | 0 dcdbsync/db/identity/__init__.py | 0 dcdbsync/db/identity/api.py | 157 +++++ dcdbsync/db/identity/sqlalchemy/__init__.py | 0 dcdbsync/db/identity/sqlalchemy/api.py | 538 ++++++++++++++++++ dcdbsync/db/identity/utils.py | 53 ++ dcdbsync/dbsyncclient/__init__.py | 0 dcdbsync/dbsyncclient/base.py | 110 ++++ dcdbsync/dbsyncclient/client.py | 60 ++ dcdbsync/dbsyncclient/exceptions.py | 106 ++++ dcdbsync/dbsyncclient/httpclient.py | 184 ++++++ dcdbsync/dbsyncclient/v1/__init__.py | 0 dcdbsync/dbsyncclient/v1/client.py | 178 ++++++ dcdbsync/dbsyncclient/v1/identity/__init__.py | 0 .../v1/identity/identity_manager.py | 202 +++++++ .../v1/identity/project_manager.py | 138 +++++ .../dbsyncclient/v1/identity/role_manager.py | 127 +++++ .../v1/identity/token_revoke_event_manager.py | 166 ++++++ dcdbsync/version.py | 20 + etc/dcdbsync/README-dcdbsync.conf.txt | 4 + etc/dcdbsync/policy.json | 5 + ocf/dcdbsync-api | 324 +++++++++++ setup.cfg | 5 +- 53 files changed, 4299 insertions(+), 1 deletion(-) create mode 100644 dcdbsync/__init__.py create mode 100755 dcdbsync/api/README.rst create mode 100644 dcdbsync/api/__init__.py create mode 100644 dcdbsync/api/api_config.py create mode 100644 dcdbsync/api/app.py create mode 100755 dcdbsync/api/controllers/README.rst create mode 100644 dcdbsync/api/controllers/__init__.py create mode 100644 dcdbsync/api/controllers/restcomm.py create mode 100644 dcdbsync/api/controllers/root.py create mode 100644 dcdbsync/api/controllers/v1/__init__.py create mode 100644 dcdbsync/api/controllers/v1/identity/__init__.py create mode 100644 dcdbsync/api/controllers/v1/identity/identity.py create mode 100644 dcdbsync/api/controllers/v1/identity/project.py create mode 100644 dcdbsync/api/controllers/v1/identity/role.py create mode 100644 dcdbsync/api/controllers/v1/identity/root.py create mode 100644 dcdbsync/api/controllers/v1/identity/token_revoke_event.py create mode 100644 dcdbsync/api/controllers/v1/root.py create mode 100644 dcdbsync/api/enforcer.py create mode 100755 dcdbsync/cmd/README.rst create mode 100644 dcdbsync/cmd/__init__.py create mode 100644 dcdbsync/cmd/api.py create mode 100644 dcdbsync/common/__init__.py create mode 100644 dcdbsync/common/config.py create mode 100644 dcdbsync/common/context.py create mode 100644 dcdbsync/common/exceptions.py create mode 100644 dcdbsync/common/i18n.py create mode 100644 dcdbsync/common/messaging.py create mode 100644 dcdbsync/common/policy.py create mode 100644 dcdbsync/common/version.py create mode 100644 dcdbsync/config-generator.conf create mode 100644 dcdbsync/db/__init__.py create mode 100644 dcdbsync/db/identity/__init__.py create mode 100644 dcdbsync/db/identity/api.py create mode 100644 dcdbsync/db/identity/sqlalchemy/__init__.py create mode 100644 dcdbsync/db/identity/sqlalchemy/api.py create mode 100644 dcdbsync/db/identity/utils.py create mode 100644 dcdbsync/dbsyncclient/__init__.py create mode 100644 dcdbsync/dbsyncclient/base.py create mode 100644 dcdbsync/dbsyncclient/client.py create mode 100644 dcdbsync/dbsyncclient/exceptions.py create mode 100644 dcdbsync/dbsyncclient/httpclient.py create mode 100644 dcdbsync/dbsyncclient/v1/__init__.py create mode 100644 dcdbsync/dbsyncclient/v1/client.py create mode 100644 dcdbsync/dbsyncclient/v1/identity/__init__.py create mode 100644 dcdbsync/dbsyncclient/v1/identity/identity_manager.py create mode 100644 dcdbsync/dbsyncclient/v1/identity/project_manager.py create mode 100644 dcdbsync/dbsyncclient/v1/identity/role_manager.py create mode 100644 dcdbsync/dbsyncclient/v1/identity/token_revoke_event_manager.py create mode 100644 dcdbsync/version.py create mode 100644 etc/dcdbsync/README-dcdbsync.conf.txt create mode 100755 etc/dcdbsync/policy.json create mode 100644 ocf/dcdbsync-api diff --git a/dcdbsync/__init__.py b/dcdbsync/__init__.py new file mode 100644 index 000000000..793684a92 --- /dev/null +++ b/dcdbsync/__init__.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- + +# 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. +# +# Copyright (c) 2019 Wind River Systems, Inc. +# +# SPDX-License-Identifier: Apache-2.0 +# + +import pbr.version + + +__version__ = pbr.version.VersionInfo('distributedcloud').version_string() diff --git a/dcdbsync/api/README.rst b/dcdbsync/api/README.rst new file mode 100755 index 000000000..7f4662f6d --- /dev/null +++ b/dcdbsync/api/README.rst @@ -0,0 +1,27 @@ +=============================== +api +=============================== + +DC DBsync API is Web Server Gateway Interface (WSGI) application to receive +and process API calls, including keystonemiddleware to do the authentication, +parameter check and validation. It receives API calls from DC Orchestrator +to read/write/update resources in Databases on behalf of DC Orchestrator. +The API calls are processed in synchronous way, so that the caller will wait +for the response to come back. + +Multiple DC DBsync API could run in parallel, and also can work in +multi-worker mode. + +Multiple DC DBsync API is designed and run in stateless mode. + +Setup and encapsulate the API WSGI app + +app.py: + Setup and encapsulate the API WSGI app, including integrate the + keystonemiddleware app + +api_config.py: + API configuration loading and init + +enforcer.py + Enforces policies on the version2 APIs diff --git a/dcdbsync/api/__init__.py b/dcdbsync/api/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/dcdbsync/api/api_config.py b/dcdbsync/api/api_config.py new file mode 100644 index 000000000..80e131cea --- /dev/null +++ b/dcdbsync/api/api_config.py @@ -0,0 +1,109 @@ +# Copyright 2015 Huawei Technologies Co., Ltd. +# All Rights Reserved. +# +# 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. +# +# Copyright (c) 2019 Wind River Systems, Inc. +# +# SPDX-License-Identifier: Apache-2.0 +# + +""" +Routines for configuring DC DBsync Agent, largely copy from Neutron +""" + +import os +import sys + + +from oslo_config import cfg +from oslo_log import log as logging + +from dcdbsync.common.i18n import _ + + +# from dcdbsync import policy +from dcdbsync.common import version + +LOG = logging.getLogger(__name__) + +common_opts = [ + cfg.StrOpt('bind_host', default='0.0.0.0', + help=_("The host IP to bind to")), + cfg.IntOpt('bind_port', default=8119, + help=_("The port to bind to")), + cfg.IntOpt('api_workers', default=2, + help=_("number of api workers")), + cfg.StrOpt('state_path', + default=os.path.join(os.path.dirname(__file__), '../'), + help='Top-level directory for maintaining dcdbsync state'), + cfg.StrOpt('api_extensions_path', default="", + help=_("The path for API extensions")), + cfg.StrOpt('auth_strategy', default='keystone', + help=_("The type of authentication to use")), + cfg.BoolOpt('allow_bulk', default=True, + help=_("Allow the usage of the bulk API")), + cfg.BoolOpt('allow_pagination', default=False, + help=_("Allow the usage of the pagination")), + cfg.BoolOpt('allow_sorting', default=False, + help=_("Allow the usage of the sorting")), + cfg.StrOpt('pagination_max_limit', default="-1", + help=_("The maximum number of items returned in a single " + "response, value was 'infinite' or negative integer " + "means no limit")), +] + + +def init(args, **kwargs): + # Register the configuration options + cfg.CONF.register_opts(common_opts) + + # ks_session.Session.register_conf_options(cfg.CONF) + # auth.register_conf_options(cfg.CONF) + logging.register_options(cfg.CONF) + + cfg.CONF(args=args, project='dcdbsync', + version='%%(prog)s %s' % version.version_info.release_string(), + **kwargs) + + +def setup_logging(): + """Sets up the logging options for a log with supplied name.""" + product_name = "dcdbsync" + logging.setup(cfg.CONF, product_name) + LOG.info("Logging enabled!") + LOG.info("%(prog)s version %(version)s", + {'prog': sys.argv[0], + 'version': version.version_info.release_string()}) + LOG.debug("command line: %s", " ".join(sys.argv)) + + +def reset_service(): + # Reset worker in case SIGHUP is called. + # Note that this is called only in case a service is running in + # daemon mode. + setup_logging() + + # TODO(joehuang) enforce policy later + # policy.refresh() + + +def test_init(): + # Register the configuration options + cfg.CONF.register_opts(common_opts) + logging.register_options(cfg.CONF) + setup_logging() + + +def list_opts(): + yield None, common_opts diff --git a/dcdbsync/api/app.py b/dcdbsync/api/app.py new file mode 100644 index 000000000..1d9ba27b6 --- /dev/null +++ b/dcdbsync/api/app.py @@ -0,0 +1,95 @@ +# Copyright (c) 2015 Huawei, Tech. Co,. Ltd. +# All Rights Reserved. +# +# 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. +# +# Copyright (c) 2019 Wind River Systems, Inc. +# +# SPDX-License-Identifier: Apache-2.0 +# + +import pecan + +from keystonemiddleware import auth_token +from oslo_config import cfg +from oslo_middleware import request_id +from oslo_service import service + +from dcdbsync.common import context as ctx +from dcdbsync.common.i18n import _ + + +def setup_app(*args, **kwargs): + + opts = cfg.CONF.pecan + config = { + 'server': { + 'port': cfg.CONF.bind_port, + 'host': cfg.CONF.bind_host + }, + 'app': { + 'root': 'dcdbsync.api.controllers.root.RootController', + 'modules': ['dcdbsync.api'], + "debug": opts.debug, + "auth_enable": opts.auth_enable, + 'errors': { + 400: '/error', + '__force_dict__': True + } + } + } + + pecan_config = pecan.configuration.conf_from_dict(config) + + # app_hooks = [], hook collection will be put here later + + app = pecan.make_app( + pecan_config.app.root, + debug=False, + wrap_app=_wrap_app, + force_canonical=False, + hooks=lambda: [ctx.AuthHook()], + guess_content_type_from_ext=True + ) + + return app + + +def _wrap_app(app): + app = request_id.RequestId(app) + if cfg.CONF.pecan.auth_enable and cfg.CONF.auth_strategy == 'keystone': + conf = dict(cfg.CONF.keystone_authtoken) + # Change auth decisions of requests to the app itself. + conf.update({'delay_auth_decision': True}) + + # NOTE: Policy enforcement works only if Keystone + # authentication is enabled. No support for other authentication + # types at this point. + return auth_token.AuthProtocol(app, conf) + else: + return app + + +_launcher = None + + +def serve(api_service, conf, workers=1): + global _launcher + if _launcher: + raise RuntimeError(_('serve() can only be called once')) + + _launcher = service.launch(conf, api_service, workers=workers) + + +def wait(): + _launcher.wait() diff --git a/dcdbsync/api/controllers/README.rst b/dcdbsync/api/controllers/README.rst new file mode 100755 index 000000000..f0a6688b1 --- /dev/null +++ b/dcdbsync/api/controllers/README.rst @@ -0,0 +1,11 @@ +=============================== +controllers +=============================== + +API request processing + +root.py: + API root request + +restcomm.py: + common functionality used in API diff --git a/dcdbsync/api/controllers/__init__.py b/dcdbsync/api/controllers/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/dcdbsync/api/controllers/restcomm.py b/dcdbsync/api/controllers/restcomm.py new file mode 100644 index 000000000..e509163cf --- /dev/null +++ b/dcdbsync/api/controllers/restcomm.py @@ -0,0 +1,46 @@ +# Copyright (c) 2015 Huawei Tech. Co., Ltd. +# All Rights Reserved. +# +# 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. +# +# Copyright (c) 2019 Wind River Systems, Inc. +# +# SPDX-License-Identifier: Apache-2.0 +# + + +from pecan import request + +import dcdbsync.common.context as k_context + + +def extract_context_from_environ(): + context_paras = {'auth_token': 'HTTP_X_AUTH_TOKEN', + 'user': 'HTTP_X_USER_ID', + 'project': 'HTTP_X_TENANT_ID', + 'user_name': 'HTTP_X_USER_NAME', + 'tenant_name': 'HTTP_X_PROJECT_NAME', + 'domain': 'HTTP_X_DOMAIN_ID', + 'roles': 'HTTP_X_ROLE', + 'user_domain': 'HTTP_X_USER_DOMAIN_ID', + 'project_domain': 'HTTP_X_PROJECT_DOMAIN_ID', + 'request_id': 'openstack.request_id'} + + environ = request.environ + + for key in context_paras: + context_paras[key] = environ.get(context_paras[key]) + role = environ.get('HTTP_X_ROLE') + + context_paras['is_admin'] = 'admin' in role.split(',') + return k_context.RequestContext(**context_paras) diff --git a/dcdbsync/api/controllers/root.py b/dcdbsync/api/controllers/root.py new file mode 100644 index 000000000..001dd7a23 --- /dev/null +++ b/dcdbsync/api/controllers/root.py @@ -0,0 +1,62 @@ +# Copyright (c) 2015 Huawei Tech. Co., Ltd. +# All Rights Reserved. +# +# 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. +# +# Copyright (c) 2019 Wind River Systems, Inc. +# +# SPDX-License-Identifier: Apache-2.0 +# + + +import pecan + +from dcdbsync.api.controllers.v1 import root as v1_root + + +class RootController(object): + + @pecan.expose('json') + def _lookup(self, version, *remainder): + version = str(version) + minor_version = version[-1] + major_version = version[1] + remainder = remainder + (minor_version,) + if major_version == '1': + return v1_root.Controller(), remainder + + @pecan.expose(generic=True, template='json') + def index(self): + return { + "versions": [ + { + "status": "CURRENT", + "links": [ + { + "rel": "self", + "href": pecan.request.application_url + "/v1.0/" + } + ], + "id": "v1.0", + "updated": "2018-11-20" + } + ] + } + + @index.when(method='POST') + @index.when(method='PUT') + @index.when(method='DELETE') + @index.when(method='HEAD') + @index.when(method='PATCH') + def not_supported(self): + pecan.abort(405) diff --git a/dcdbsync/api/controllers/v1/__init__.py b/dcdbsync/api/controllers/v1/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/dcdbsync/api/controllers/v1/identity/__init__.py b/dcdbsync/api/controllers/v1/identity/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/dcdbsync/api/controllers/v1/identity/identity.py b/dcdbsync/api/controllers/v1/identity/identity.py new file mode 100644 index 000000000..6f9bdcaac --- /dev/null +++ b/dcdbsync/api/controllers/v1/identity/identity.py @@ -0,0 +1,133 @@ +# Copyright (c) 2017 Ericsson AB. +# All Rights Reserved. +# +# 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. +# +# Copyright (c) 2019 Wind River Systems, Inc. +# +# SPDX-License-Identifier: Apache-2.0 +# + +from oslo_config import cfg +from oslo_log import log as logging + +import pecan +from pecan import expose +from pecan import request +from pecan import response + +from dcdbsync.api.controllers import restcomm +from dcdbsync.common import exceptions +from dcdbsync.common.i18n import _ +from dcdbsync.db.identity import api as db_api + +import json + +CONF = cfg.CONF +LOG = logging.getLogger(__name__) + + +class UsersController(object): + VERSION_ALIASES = { + 'Pike': '1.0', + } + + def __init__(self): + super(UsersController, self).__init__() + + # to do the version compatibility for future purpose + def _determine_version_cap(self, target): + version_cap = 1.0 + return version_cap + + @expose(generic=True, template='json') + def index(self): + # Route the request to specific methods with parameters + pass + + @index.when(method='GET', template='json') + def get(self, user_ref=None): + """Get a list of users.""" + context = restcomm.extract_context_from_environ() + try: + if user_ref is None: + return db_api.user_get_all(context) + + else: + user = db_api.user_get(context, user_ref) + return user + + except exceptions.UserNotFound as e: + pecan.abort(404, _("User not found: %s") % e) + + except Exception as e: + LOG.exception(e) + pecan.abort(500, _('Unable to get user')) + + @index.when(method='POST', template='json') + def post(self): + """Create a new user.""" + + context = restcomm.extract_context_from_environ() + + # Convert JSON string in request to Python dict + try: + payload = json.loads(request.body) + except ValueError: + pecan.abort(400, _('Request body decoding error')) + + if not payload: + pecan.abort(400, _('Body required')) + user_name = payload.get('local_user').get('name') + + if not user_name: + pecan.abort(400, _('User name required')) + + try: + # Insert the user into DB tables + user_ref = db_api.user_create(context, payload) + response.status = 201 + return (user_ref) + + except Exception as e: + LOG.exception(e) + pecan.abort(500, _('Unable to create user')) + + @index.when(method='PUT', template='json') + def put(self, user_ref=None): + """Update a existing user.""" + + context = restcomm.extract_context_from_environ() + + if user_ref is None: + pecan.abort(400, _('User ID required')) + + # Convert JSON string in request to Python dict + try: + payload = json.loads(request.body) + except ValueError: + pecan.abort(400, _('Request body decoding error')) + + if not payload: + pecan.abort(400, _('Body required')) + + try: + # Update the user in DB tables + return db_api.user_update(context, user_ref, payload) + + except exceptions.UserNotFound as e: + pecan.abort(404, _("User not found: %s") % e) + + except Exception as e: + LOG.exception(e) + pecan.abort(500, _('Unable to update user')) diff --git a/dcdbsync/api/controllers/v1/identity/project.py b/dcdbsync/api/controllers/v1/identity/project.py new file mode 100644 index 000000000..00e036fd0 --- /dev/null +++ b/dcdbsync/api/controllers/v1/identity/project.py @@ -0,0 +1,134 @@ +# Copyright (c) 2017 Ericsson AB. +# All Rights Reserved. +# +# 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. +# +# Copyright (c) 2019 Wind River Systems, Inc. +# +# SPDX-License-Identifier: Apache-2.0 +# + +from oslo_config import cfg +from oslo_log import log as logging + +import pecan +from pecan import expose +from pecan import request +from pecan import response + +from dcdbsync.api.controllers import restcomm +from dcdbsync.common import exceptions +from dcdbsync.common.i18n import _ +from dcdbsync.db.identity import api as db_api + +import json + +CONF = cfg.CONF +LOG = logging.getLogger(__name__) + + +class ProjectsController(object): + VERSION_ALIASES = { + 'Pike': '1.0', + } + + def __init__(self): + super(ProjectsController, self).__init__() + + # to do the version compatibility for future purpose + def _determine_version_cap(self, target): + version_cap = 1.0 + return version_cap + + @expose(generic=True, template='json') + def index(self): + # Route the request to specific methods with parameters + pass + + @index.when(method='GET', template='json') + def get(self, project_ref=None): + + context = restcomm.extract_context_from_environ() + + try: + if project_ref is None: + return db_api.project_get_all(context) + + else: + project = db_api.project_get(context, project_ref) + return project + + except exceptions.ProjectNotFound as e: + pecan.abort(404, _("Project not found: %s") % e) + + except Exception as e: + LOG.exception(e) + pecan.abort(500, _('Unable to get project')) + + @index.when(method='POST', template='json') + def post(self): + """Create a new project.""" + + context = restcomm.extract_context_from_environ() + + try: + payload = json.loads(request.body) + except ValueError: + pecan.abort(400, _('Request body decoding error')) + + if not payload: + pecan.abort(400, _('Body required')) + project_name = payload.get('project').get('name') + + if not project_name: + pecan.abort(400, _('project name required')) + + try: + # Insert the project into DB tables + project_ref = db_api.project_create(context, payload) + response.status = 201 + return project_ref + + except Exception as e: + LOG.exception(e) + pecan.abort(500, _('Unable to create project')) + + @index.when(method='PUT', template='json') + def put(self, project_ref=None): + """Update a existing project.""" + + context = restcomm.extract_context_from_environ() + + if project_ref is None: + pecan.abort(400, _('Project ID required')) + + # Convert JSON string in request to Python dict + try: + payload = json.loads(request.body) + except ValueError: + pecan.abort(400, _('Request body decoding error')) + + if not payload: + pecan.abort(400, _('Body required')) + + try: + # Update the project in DB tables + project_ref = db_api.project_update(context, project_ref, payload) + return project_ref + + except exceptions.ProjectNotFound as e: + pecan.abort(404, _("Project not found: %s") % e) + + except Exception as e: + LOG.exception(e) + pecan.abort(500, _('Unable to update project')) diff --git a/dcdbsync/api/controllers/v1/identity/role.py b/dcdbsync/api/controllers/v1/identity/role.py new file mode 100644 index 000000000..0e7074b69 --- /dev/null +++ b/dcdbsync/api/controllers/v1/identity/role.py @@ -0,0 +1,135 @@ +# Copyright (c) 2017 Ericsson AB. +# All Rights Reserved. +# +# 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. +# +# Copyright (c) 2019 Wind River Systems, Inc. +# +# SPDX-License-Identifier: Apache-2.0 +# + +from oslo_config import cfg +from oslo_log import log as logging + +import pecan +from pecan import expose +from pecan import request +from pecan import response + +from dcdbsync.api.controllers import restcomm +from dcdbsync.common import exceptions +from dcdbsync.common.i18n import _ +from dcdbsync.db.identity import api as db_api + +import json + +CONF = cfg.CONF +LOG = logging.getLogger(__name__) + + +class RolesController(object): + VERSION_ALIASES = { + 'Pike': '1.0', + } + + def __init__(self): + super(RolesController, self).__init__() + + # to do the version compatibility for future purpose + def _determine_version_cap(self, target): + version_cap = 1.0 + return version_cap + + @expose(generic=True, template='json') + def index(self): + # Route the request to specific methods with parameters + pass + + @index.when(method='GET', template='json') + def get(self, role_ref=None): + """Get a list of roles.""" + context = restcomm.extract_context_from_environ() + + try: + if role_ref is None: + return db_api.role_get_all(context) + + else: + role = db_api.role_get(context, role_ref) + return role + + except exceptions.RoleNotFound as e: + pecan.abort(404, _("Role not found: %s") % e) + + except Exception as e: + LOG.exception(e) + pecan.abort(500, _('Unable to get role')) + + @index.when(method='POST', template='json') + def post(self): + """Create a new role.""" + + context = restcomm.extract_context_from_environ() + + # Convert JSON string in request to Python dict + try: + payload = json.loads(request.body) + except ValueError: + pecan.abort(400, _('Request body decoding error')) + + if not payload: + pecan.abort(400, _('Body required')) + role_name = payload.get('role').get('name') + + if not role_name: + pecan.abort(400, _('role name required')) + + try: + # Insert the role into DB tables + role_ref = db_api.role_create(context, payload) + response.status = 201 + return role_ref + + except Exception as e: + LOG.exception(e) + pecan.abort(500, _('Unable to create role')) + + @index.when(method='PUT', template='json') + def put(self, role_ref=None): + """Update a existing role.""" + + context = restcomm.extract_context_from_environ() + + if role_ref is None: + pecan.abort(400, _('Role ID required')) + + # Convert JSON string in request to Python dict + try: + payload = json.loads(request.body) + except ValueError: + pecan.abort(400, _('Request body decoding error')) + + if not payload: + pecan.abort(400, _('Body required')) + + try: + # Update the role in DB tables + role_ref = db_api.role_update(context, role_ref, payload) + return role_ref + + except exceptions.RoleNotFound as e: + pecan.abort(404, _("Role not found: %s") % e) + + except Exception as e: + LOG.exception(e) + pecan.abort(500, _('Unable to update role')) diff --git a/dcdbsync/api/controllers/v1/identity/root.py b/dcdbsync/api/controllers/v1/identity/root.py new file mode 100644 index 000000000..a09885964 --- /dev/null +++ b/dcdbsync/api/controllers/v1/identity/root.py @@ -0,0 +1,63 @@ +# Copyright (c) 2017 Ericsson AB. +# All Rights Reserved. +# +# 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. +# +# Copyright (c) 2019 Wind River Systems, Inc. +# +# SPDX-License-Identifier: Apache-2.0 +# + +from oslo_config import cfg +from oslo_log import log as logging + +import pecan + +from dcdbsync.api.controllers.v1.identity import identity +from dcdbsync.api.controllers.v1.identity import project +from dcdbsync.api.controllers.v1.identity import role +from dcdbsync.api.controllers.v1.identity import token_revoke_event + +CONF = cfg.CONF +LOG = logging.getLogger(__name__) + + +class IdentityController(object): + + def _get_resource_controller(self, remainder): + + if not remainder: + pecan.abort(404) + return + + res_controllers = dict() + res_controllers["users"] = identity.UsersController + res_controllers["projects"] = project.ProjectsController + res_controllers["roles"] = role.RolesController + res_controllers["token-revocation-events"] = \ + token_revoke_event.RevokeEventsController + + for name, ctrl in res_controllers.items(): + setattr(self, name, ctrl) + + resource = remainder[0] + if resource not in res_controllers: + pecan.abort(404) + return + + remainder = remainder[1:] + return res_controllers[resource](), remainder + + @pecan.expose() + def _lookup(self, *remainder): + return self._get_resource_controller(remainder) diff --git a/dcdbsync/api/controllers/v1/identity/token_revoke_event.py b/dcdbsync/api/controllers/v1/identity/token_revoke_event.py new file mode 100644 index 000000000..69ec5669a --- /dev/null +++ b/dcdbsync/api/controllers/v1/identity/token_revoke_event.py @@ -0,0 +1,247 @@ +# Copyright (c) 2017 Ericsson AB. +# All Rights Reserved. +# +# 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. +# +# Copyright (c) 2019 Wind River Systems, Inc. +# +# SPDX-License-Identifier: Apache-2.0 +# + +from oslo_config import cfg +from oslo_log import log as logging + +import base64 +import pecan +from pecan import expose +from pecan import request +from pecan import response + +from dcdbsync.api.controllers import restcomm +from dcdbsync.common import exceptions +from dcdbsync.common.i18n import _ +from dcdbsync.db.identity import api as db_api + +import json + +CONF = cfg.CONF +LOG = logging.getLogger(__name__) + + +class RevokeEventsController(object): + VERSION_ALIASES = { + 'Pike': '1.0', + } + + def __init__(self): + super(RevokeEventsController, self).__init__() + + # to do the version compatibility for future purpose + def _determine_version_cap(self, target): + version_cap = 1.0 + return version_cap + + @expose(generic=True, template='json') + def index(self): + # Route the request to specific methods with parameters + pass + + @index.when(method='POST', template='json') + def post(self): + """Create a new token revoke event.""" + + context = restcomm.extract_context_from_environ() + + # Convert JSON string in request to Python dict + try: + payload = json.loads(request.body) + except ValueError: + pecan.abort(400, _('Request body decoding error')) + + if not payload: + pecan.abort(400, _('Body required')) + + try: + # Insert the token revoke event into DB tables + revoke_event_ref = db_api.revoke_event_create(context, payload) + response.status = 201 + return revoke_event_ref + + except Exception as e: + LOG.exception(e) + pecan.abort(500, _('Unable to create token revocation event')) + + @index.when(method='GET', template='json') + def get(self): + """Get all of token revoke events.""" + context = restcomm.extract_context_from_environ() + + try: + return db_api.revoke_event_get_all(context) + + except Exception as e: + LOG.exception(e) + pecan.abort(500, _('Unable to get token revocation events')) + + def _get_resource_controller(self, remainder): + if not remainder: + pecan.abort(404) + return + + res_controllers = dict() + res_controllers["audits"] = AuditsController + res_controllers["users"] = UsersController + + for name, ctrl in res_controllers.items(): + setattr(self, name, ctrl) + + resource = remainder[0] + if resource not in res_controllers: + pecan.abort(404) + return + + remainder = remainder[1:] + return res_controllers[resource](), remainder + + @pecan.expose() + def _lookup(self, *remainder): + return self._get_resource_controller(remainder) + + +class UsersController(object): + def __init__(self): + super(UsersController, self).__init__() + + @expose(generic=True, template='json') + def index(self): + # Route the request to specific methods with parameters + pass + + @index.when(method='GET', template='json') + def get(self, event_id=None): + """Get a token revoke event by user_id and issued_before.""" + + context = restcomm.extract_context_from_environ() + + if event_id is None: + pecan.abort(400, _('Event ID required')) + + try: + # user specific event id is in the format of + # _ and encoded in base64 + event_ref = base64.urlsafe_b64decode(str(event_id)) + event_tags = event_ref.split('_') + user_id = event_tags[0] + issued_before = event_tags[1] + + revoke_event = db_api.\ + revoke_event_get_by_user(context, user_id=user_id, + issued_before=issued_before) + return revoke_event + + except (IndexError, TypeError): + pecan.abort(404, _('Invalid event ID format')) + except exceptions.RevokeEventNotFound: + unique_id = "user_id {} and issued_before {}".\ + format(user_id, issued_before) + pecan.abort(404, _("Token revocation event %s doesn't exist.") + % unique_id) + + except Exception as e: + LOG.exception(e) + pecan.abort(500, _('Unable to get token revocation event')) + + @index.when(method='DELETE') + def delete(self, event_id=None): + """Delete a token revoke event by user_id and issued_before.""" + + context = restcomm.extract_context_from_environ() + + if event_id is None: + pecan.abort(400, _('Event ID required')) + + try: + # user specific event id is in the format of + # _ and encoded in base64 + event_ref = base64.urlsafe_b64decode(str(event_id)) + event_tags = event_ref.split('_') + user_id = event_tags[0] + issued_before = event_tags[1] + db_api.revoke_event_delete_by_user(context, user_id=user_id, + issued_before=issued_before) + response.headers['Content-Type'] = None + + except (IndexError, TypeError): + pecan.abort(404, _('Invalid event ID format')) + except exceptions.RevokeEventNotFound: + unique_id = "user_id {} and issued_before {}".\ + format(user_id, issued_before) + pecan.abort(404, _("Token revocation event %s doesn't exist.") + % unique_id) + + except Exception as e: + LOG.exception(e) + pecan.abort(500, _('Unable to delete token revocation event')) + + +class AuditsController(object): + def __init__(self): + super(AuditsController, self).__init__() + + @expose(generic=True, template='json') + def index(self): + # Route the request to specific methods with parameters + pass + + @index.when(method='GET', template='json') + def get(self, audit_id=None): + """Get a token revoke event by revocation_event.audit_id.""" + + context = restcomm.extract_context_from_environ() + + if audit_id is None: + pecan.abort(400, _('Audit ID required')) + + try: + revoke_event = db_api.\ + revoke_event_get_by_audit(context, audit_id=audit_id) + return revoke_event + + except exceptions.RevokeEventNotFound: + pecan.abort(404, _("Token revocation event with id %s" + " doesn't exist.") % audit_id) + + except Exception as e: + LOG.exception(e) + pecan.abort(500, _('Unable to get token revocation event')) + + @index.when(method='DELETE') + def delete(self, audit_id=None): + """Delete a token revoke event by revocation_event.audit_id.""" + + context = restcomm.extract_context_from_environ() + + if audit_id is None: + pecan.abort(400, _('Audit ID required')) + + try: + db_api.revoke_event_delete_by_audit(context, audit_id=audit_id) + response.headers['Content-Type'] = None + + except exceptions.RevokeEventNotFound: + pecan.abort(404, _("Token revocation event with id %s" + " doesn't exist.") % audit_id) + + except Exception as e: + LOG.exception(e) + pecan.abort(500, _('Unable to delete token revocation event')) diff --git a/dcdbsync/api/controllers/v1/root.py b/dcdbsync/api/controllers/v1/root.py new file mode 100644 index 000000000..351812b0a --- /dev/null +++ b/dcdbsync/api/controllers/v1/root.py @@ -0,0 +1,60 @@ +# Copyright (c) 2017 Ericsson AB. +# All Rights Reserved. +# +# 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. +# +# Copyright (c) 2019 Wind River Systems, Inc. +# +# SPDX-License-Identifier: Apache-2.0 +# + + +from dcdbsync.api.controllers.v1.identity import root + +import pecan + + +class Controller(object): + + def _get_sub_controller(self, remainder): + + if not remainder: + pecan.abort(404) + return + + minor_version = remainder[-1] + remainder = remainder[:-1] + + sub_controllers = dict() + if minor_version == '0': + sub_controllers["identity"] = root.IdentityController + + for name, ctrl in sub_controllers.items(): + setattr(self, name, ctrl) + + try: + sub_controller = remainder[0] + except IndexError: + pecan.abort(404) + return + + if sub_controller not in sub_controllers: + pecan.abort(404) + return + + remainder = remainder[1:] + return sub_controllers[sub_controller](), remainder + + @pecan.expose() + def _lookup(self, *remainder): + return self._get_sub_controller(remainder) diff --git a/dcdbsync/api/enforcer.py b/dcdbsync/api/enforcer.py new file mode 100644 index 000000000..ae6c3b27a --- /dev/null +++ b/dcdbsync/api/enforcer.py @@ -0,0 +1,76 @@ +# Copyright 2017 Ericsson AB. +# +# 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. +# +# Copyright (c) 2019 Wind River Systems, Inc. +# +# SPDX-License-Identifier: Apache-2.0 +# + +"""Policy enforcer for DC DBsync Agent.""" + +from oslo_config import cfg +from oslo_policy import policy + +from dcdbsync.common import exceptions as exc + + +_ENFORCER = None + + +def enforce(action, context, target=None, do_raise=True, + exc=exc.NotAuthorized): + """Verify that the action is valid on the target in this context. + + :param action: String, representing the action to be checked. + This should be colon separated for clarity. + i.e. ``sync:list`` + :param context: DC DBsync context. + :param target: Dictionary, representing the object of the action. + For object creation, this should be a dictionary + representing the location of the object. + e.g. ``{'project_id': context.project}`` + :param do_raise: if True (the default), raises specified exception. + :param exc: Exception to be raised if not authorized. Default is + dcdbsync.common.exceptions.NotAuthorized. + + :return: returns True if authorized and False if not authorized and + do_raise is False. + """ + if cfg.CONF.auth_strategy != 'keystone': + # Policy enforcement is supported now only with Keystone + # authentication. + return + + target_obj = { + 'project_id': context.project, + 'user_id': context.user, + } + + target_obj.update(target or {}) + _ensure_enforcer_initialization() + + try: + _ENFORCER.enforce(action, target_obj, context.to_dict(), + do_raise=do_raise, exc=exc) + return True + + except Exception: + return False + + +def _ensure_enforcer_initialization(): + global _ENFORCER + if not _ENFORCER: + _ENFORCER = policy.Enforcer(cfg.CONF) + _ENFORCER.load_rules() diff --git a/dcdbsync/cmd/README.rst b/dcdbsync/cmd/README.rst new file mode 100755 index 000000000..befb886ec --- /dev/null +++ b/dcdbsync/cmd/README.rst @@ -0,0 +1,9 @@ +=============================== +cmd +=============================== + +Scripts to start the DC dbsync API service + +api.py: + start API service + python api.py --config-file=/etc/dcdbsync/dcdbsync.conf diff --git a/dcdbsync/cmd/__init__.py b/dcdbsync/cmd/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/dcdbsync/cmd/api.py b/dcdbsync/cmd/api.py new file mode 100644 index 000000000..898891c87 --- /dev/null +++ b/dcdbsync/cmd/api.py @@ -0,0 +1,75 @@ +# Copyright 2015 Huawei Technologies Co., Ltd. +# All Rights Reserved. +# +# 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. +# +# Copyright (c) 2019 Wind River Systems, Inc. +# +# SPDX-License-Identifier: Apache-2.0 +# + +# Much of this module is based on the work of the Ironic team +# see http://git.openstack.org/cgit/openstack/ironic/tree/ironic/cmd/api.py + + +import sys + +import eventlet +from oslo_config import cfg +from oslo_log import log as logging +from oslo_service import systemd +from oslo_service import wsgi + +import logging as std_logging + +from dcdbsync.api import api_config +from dcdbsync.api import app + +from dcdbsync.common import config +from dcdbsync.common import messaging + +CONF = cfg.CONF +config.register_options() +LOG = logging.getLogger('dcdbsync.api') +eventlet.monkey_patch(os=False) + + +def main(): + api_config.init(sys.argv[1:]) + api_config.setup_logging() + application = app.setup_app() + + host = CONF.bind_host + port = CONF.bind_port + workers = CONF.api_workers + + if workers < 1: + LOG.warning("Wrong worker number, worker = %(workers)s", workers) + workers = 1 + + LOG.info("Server on http://%(host)s:%(port)s with %(workers)s", + {'host': host, 'port': port, 'workers': workers}) + messaging.setup() + systemd.notify_once() + service = wsgi.Server(CONF, "DCDBsync", application, host, port) + + app.serve(service, CONF, workers) + + LOG.info("Configuration:") + CONF.log_opt_values(LOG, std_logging.INFO) + + app.wait() + + +if __name__ == '__main__': + main() diff --git a/dcdbsync/common/__init__.py b/dcdbsync/common/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/dcdbsync/common/config.py b/dcdbsync/common/config.py new file mode 100644 index 000000000..e7a5f430c --- /dev/null +++ b/dcdbsync/common/config.py @@ -0,0 +1,124 @@ +# Copyright 2016 Ericsson AB +# 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. +# +# Copyright (c) 2019 Wind River Systems, Inc. +# +# SPDX-License-Identifier: Apache-2.0 +# + +""" +File to store all the configurations +""" +from oslo_config import cfg +from oslo_utils import importutils + +# Ensure keystonemiddleware options are imported +importutils.import_module('keystonemiddleware.auth_token') + +# OpenStack credentials used for Endpoint Cache +# We need to register the below non-standard config +# options to dbsync engine +keystone_opts = [ + cfg.StrOpt('username', + help='Username of account'), + cfg.StrOpt('password', + help='Password of account'), + cfg.StrOpt('project_name', + help='Tenant name of account'), + cfg.StrOpt('user_domain_name', + default='Default', + help='User domain name of account'), + cfg.StrOpt('project_domain_name', + default='Default', + help='Project domain name of account'), +] + + +# Pecan_opts +pecan_opts = [ + cfg.StrOpt( + 'root', + default='dcdbsync.api.controllers.root.RootController', + help='Pecan root controller' + ), + cfg.ListOpt( + 'modules', + default=["dcdbsync.api"], + help='A list of modules where pecan will search for applications.' + ), + cfg.BoolOpt( + 'debug', + default=False, + help='Enables the ability to display tracebacks in the browser and' + 'interactively debug during development.' + ), + cfg.BoolOpt( + 'auth_enable', + default=True, + help='Enables user authentication in pecan.' + ) +] + + +# OpenStack credentials used for Endpoint Cache +cache_opts = [ + cfg.StrOpt('auth_uri', + help='Keystone authorization url'), + cfg.StrOpt('identity_uri', + help='Keystone service url'), + cfg.StrOpt('admin_username', + help='Username of admin account, needed when' + ' auto_refresh_endpoint set to True'), + cfg.StrOpt('admin_password', + help='Password of admin account, needed when' + ' auto_refresh_endpoint set to True'), + cfg.StrOpt('admin_tenant', + help='Tenant name of admin account, needed when' + ' auto_refresh_endpoint set to True'), + cfg.StrOpt('admin_user_domain_name', + default='Default', + help='User domain name of admin account, needed when' + ' auto_refresh_endpoint set to True'), + cfg.StrOpt('admin_project_domain_name', + default='Default', + help='Project domain name of admin account, needed when' + ' auto_refresh_endpoint set to True') +] + +common_opts = [ + cfg.IntOpt('workers', default=1, + help='number of workers'), + cfg.StrOpt('host', + default='localhost', + help='hostname of the machine') +] + +keystone_opt_group = cfg.OptGroup(name='keystone_authtoken', + title='Keystone options') +# The group stores the pecan configurations. +pecan_group = cfg.OptGroup(name='pecan', + title='Pecan options') + +cache_opt_group = cfg.OptGroup(name='cache', + title='OpenStack Credentials') + + +def list_opts(): + yield cache_opt_group.name, cache_opts + yield pecan_group.name, pecan_opts + yield None, common_opts + + +def register_options(): + for group, opts in list_opts(): + cfg.CONF.register_opts(opts, group=group) diff --git a/dcdbsync/common/context.py b/dcdbsync/common/context.py new file mode 100644 index 000000000..632a7d16e --- /dev/null +++ b/dcdbsync/common/context.py @@ -0,0 +1,146 @@ +# 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. +# +# Copyright (c) 2019 Wind River Systems, Inc. +# +# SPDX-License-Identifier: Apache-2.0 +# + +import pecan +from pecan import hooks + +from oslo_context import context as base_context +from oslo_utils import encodeutils + +from dcdbsync.common import policy +from dcdbsync.db.identity import api as db_api + +ALLOWED_WITHOUT_AUTH = '/' + + +class RequestContext(base_context.RequestContext): + '''Stores information about the security context. + + The context encapsulates information related to the user accessing the + the system, as well as additional request information. + ''' + + def __init__(self, auth_token=None, user=None, project=None, + domain=None, user_domain=None, project_domain=None, + is_admin=None, read_only=False, show_deleted=False, + request_id=None, auth_url=None, trusts=None, + user_name=None, project_name=None, domain_name=None, + user_domain_name=None, project_domain_name=None, + auth_token_info=None, region_name=None, roles=None, + password=None, **kwargs): + + '''Initializer of request context.''' + # We still have 'tenant' param because oslo_context still use it. + super(RequestContext, self).__init__( + auth_token=auth_token, user=user, tenant=project, + domain=domain, user_domain=user_domain, + project_domain=project_domain, roles=roles, + read_only=read_only, show_deleted=show_deleted, + request_id=request_id) + + # request_id might be a byte array + self.request_id = encodeutils.safe_decode(self.request_id) + + # we save an additional 'project' internally for use + self.project = project + + # Session for DB access + self._session = None + + self.auth_url = auth_url + self.trusts = trusts + + self.user_name = user_name + self.project_name = project_name + self.domain_name = domain_name + self.user_domain_name = user_domain_name + self.project_domain_name = project_domain_name + + self.auth_token_info = auth_token_info + self.region_name = region_name + self.roles = roles or [] + self.password = password + + # Check user is admin or not + if is_admin is None: + self.is_admin = policy.enforce(self, 'context_is_admin', + target={'project': self.project}, + do_raise=False) + else: + self.is_admin = is_admin + + @property + def session(self): + if self._session is None: + self._session = db_api.get_session() + return self._session + + def to_dict(self): + return { + 'auth_url': self.auth_url, + 'auth_token': self.auth_token, + 'auth_token_info': self.auth_token_info, + 'user': self.user, + 'user_name': self.user_name, + 'user_domain': self.user_domain, + 'user_domain_name': self.user_domain_name, + 'project': self.project, + 'project_name': self.project_name, + 'project_domain': self.project_domain, + 'project_domain_name': self.project_domain_name, + 'domain': self.domain, + 'domain_name': self.domain_name, + 'trusts': self.trusts, + 'region_name': self.region_name, + 'roles': self.roles, + 'show_deleted': self.show_deleted, + 'is_admin': self.is_admin, + 'request_id': self.request_id, + 'password': self.password, + } + + @classmethod + def from_dict(cls, values): + return cls(**values) + + +def get_admin_context(show_deleted=False): + return RequestContext(is_admin=True, show_deleted=show_deleted) + + +def get_service_context(**args): + '''An abstraction layer for getting service context.''' + + pass + + +class AuthHook(hooks.PecanHook): + def before(self, state): + if state.request.path == ALLOWED_WITHOUT_AUTH: + return + req = state.request + identity_status = req.headers.get('X-Identity-Status') + service_identity_status = req.headers.get('X-Service-Identity-Status') + if (identity_status == 'Confirmed' or + service_identity_status == 'Confirmed'): + return + if req.headers.get('X-Auth-Token'): + msg = 'Auth token is invalid: %s' % req.headers['X-Auth-Token'] + else: + msg = 'Authentication required' + msg = "Failed to validate access token: %s" % str(msg) + pecan.abort(status_code=401, detail=msg) diff --git a/dcdbsync/common/exceptions.py b/dcdbsync/common/exceptions.py new file mode 100644 index 000000000..f2bbd6fbf --- /dev/null +++ b/dcdbsync/common/exceptions.py @@ -0,0 +1,93 @@ +# Copyright 2015 Huawei Technologies Co., Ltd. +# Copyright 2015 Ericsson AB. +# All Rights Reserved. +# +# 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. +# +# Copyright (c) 2019 Wind River Systems, Inc. +# +# SPDX-License-Identifier: Apache-2.0 +# + +""" +DBsync agent base exception handling. +""" +import six + +from oslo_utils import encodeutils +from oslo_utils import excutils + +from dcdbsync.common.i18n import _ + + +class DBsyncException(Exception): + """Base DB sync agent Exception. + + To correctly use this class, inherit from it and define + a 'message' property. That message will get printf'd + with the keyword arguments provided to the constructor. + """ + + message = _("An unknown exception occurred.") + + def __init__(self, **kwargs): + try: + super(DBsyncException, self).__init__(self.message % kwargs) + self.msg = self.message % kwargs + except Exception: + with excutils.save_and_reraise_exception() as ctxt: + if not self.use_fatal_exceptions(): + ctxt.reraise = False + # at least get the core message out if something happened + super(DBsyncException, self).__init__(self.message) + + if six.PY2: + def __unicode__(self): + return encodeutils.exception_to_unicode(self.msg) + + def use_fatal_exceptions(self): + return False + + +class NotFound(DBsyncException): + message = _("Not found") + + +class NotAuthorized(DBsyncException): + message = _("Not authorized.") + + +class AdminRequired(NotAuthorized): + message = _("User does not have admin privileges: %(reason)s") + + +class UserNotFound(NotFound): + message = _("User with id %(user_id)s doesn't exist.") + + +class ProjectNotFound(NotFound): + message = _("Project with id %(project_id)s doesn't exist.") + + +class RoleNotFound(NotFound): + message = _("Role with id %(role_id)s doesn't exist.") + + +class ProjectRoleAssignmentNotFound(NotFound): + message = _("Project role assignment with id" + " %(project_role_assignment_id)s doesn't exist.") + + +class RevokeEventNotFound(NotFound): + message = _("Token revocation event with id %(revoke_event_id)s" + " doesn't exist.") diff --git a/dcdbsync/common/i18n.py b/dcdbsync/common/i18n.py new file mode 100644 index 000000000..3d5a2b9e0 --- /dev/null +++ b/dcdbsync/common/i18n.py @@ -0,0 +1,25 @@ +# All Rights Reserved. +# +# 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. +# +# Copyright (c) 2019 Wind River Systems, Inc. +# +# SPDX-License-Identifier: Apache-2.0 +# + +import oslo_i18n + +_translators = oslo_i18n.TranslatorFactory(domain='dbsync') + +# The primary translation function using the well-known name "_" +_ = _translators.primary diff --git a/dcdbsync/common/messaging.py b/dcdbsync/common/messaging.py new file mode 100644 index 000000000..7a2382863 --- /dev/null +++ b/dcdbsync/common/messaging.py @@ -0,0 +1,116 @@ +# 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. +# +# Copyright (c) 2019 Wind River Systems, Inc. +# +# SPDX-License-Identifier: Apache-2.0 +# + +import eventlet + +from oslo_config import cfg +import oslo_messaging +from oslo_serialization import jsonutils + +from dcdbsync.common import context + +TRANSPORT = None +NOTIFIER = None + +_ALIASES = { + 'dcdbsync.openstack.common.rpc.impl_kombu': 'rabbit', + 'dcdbsync.openstack.common.rpc.impl_qpid': 'qpid', + 'dcdbsync.openstack.common.rpc.impl_zmq': 'zmq', +} + + +class RequestContextSerializer(oslo_messaging.Serializer): + def __init__(self, base): + self._base = base + + def serialize_entity(self, ctxt, entity): + if not self._base: + return entity + return self._base.serialize_entity(ctxt, entity) + + def deserialize_entity(self, ctxt, entity): + if not self._base: + return entity + return self._base.deserialize_entity(ctxt, entity) + + @staticmethod + def serialize_context(ctxt): + return ctxt.to_dict() + + @staticmethod + def deserialize_context(ctxt): + return context.RequestContext.from_dict(ctxt) + + +class JsonPayloadSerializer(oslo_messaging.NoOpSerializer): + @classmethod + def serialize_entity(cls, context, entity): + return jsonutils.to_primitive(entity, convert_instances=True) + + +def setup(url=None, optional=False): + """Initialise the oslo_messaging layer.""" + global TRANSPORT, NOTIFIER + + if url and url.startswith("fake://"): + # NOTE: oslo_messaging fake driver uses time.sleep + # for task switch, so we need to monkey_patch it + eventlet.monkey_patch(time=True) + + if not TRANSPORT: + oslo_messaging.set_transport_defaults('dcdbsync') + exmods = ['dcdbsync.common.exception'] + try: + TRANSPORT = oslo_messaging.get_transport( + cfg.CONF, url, allowed_remote_exmods=exmods, aliases=_ALIASES) + except oslo_messaging.InvalidTransportURL as e: + TRANSPORT = None + if not optional or e.url: + raise + + if not NOTIFIER and TRANSPORT: + serializer = RequestContextSerializer(JsonPayloadSerializer()) + NOTIFIER = oslo_messaging.Notifier(TRANSPORT, serializer=serializer) + + +def cleanup(): + """Cleanup the oslo_messaging layer.""" + global TRANSPORT, NOTIFIER + if TRANSPORT: + TRANSPORT.cleanup() + TRANSPORT = NOTIFIER = None + + +def get_rpc_server(target, endpoint): + """Return a configured oslo_messaging rpc server.""" + serializer = RequestContextSerializer(JsonPayloadSerializer()) + return oslo_messaging.get_rpc_server(TRANSPORT, target, [endpoint], + executor='eventlet', + serializer=serializer) + + +def get_rpc_client(**kwargs): + """Return a configured oslo_messaging RPCClient.""" + target = oslo_messaging.Target(**kwargs) + serializer = RequestContextSerializer(JsonPayloadSerializer()) + return oslo_messaging.RPCClient(TRANSPORT, target, + serializer=serializer) + + +def get_notifier(publisher_id): + """Return a configured oslo_messaging notifier.""" + return NOTIFIER.prepare(publisher_id=publisher_id) diff --git a/dcdbsync/common/policy.py b/dcdbsync/common/policy.py new file mode 100644 index 000000000..17114f61d --- /dev/null +++ b/dcdbsync/common/policy.py @@ -0,0 +1,54 @@ +# 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. +# +# Copyright (c) 2019 Wind River Systems, Inc. +# +# SPDX-License-Identifier: Apache-2.0 +# + +""" +Policy Engine For DC DBsync Agent +""" + +# from oslo_concurrency import lockutils +from oslo_config import cfg +from oslo_policy import policy + +from dcdbsync.common import exceptions + +POLICY_ENFORCER = None +CONF = cfg.CONF + + +# @lockutils.synchronized('policy_enforcer', 'dcdbsync-') +def _get_enforcer(policy_file=None, rules=None, default_rule=None): + + global POLICY_ENFORCER + + if POLICY_ENFORCER is None: + POLICY_ENFORCER = policy.Enforcer(CONF, + policy_file=policy_file, + rules=rules, + default_rule=default_rule) + return POLICY_ENFORCER + + +def enforce(context, rule, target, do_raise=True, *args, **kwargs): + + enforcer = _get_enforcer() + credentials = context.to_dict() + target = target or {} + if do_raise: + kwargs.update(exc=exceptions.Forbidden) + + return enforcer.enforce(rule, target, credentials, do_raise, + *args, **kwargs) diff --git a/dcdbsync/common/version.py b/dcdbsync/common/version.py new file mode 100644 index 000000000..5c64c4077 --- /dev/null +++ b/dcdbsync/common/version.py @@ -0,0 +1,46 @@ +# Copyright 2011 OpenStack Foundation +# +# 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. +# +# Copyright (c) 2019 Wind River Systems, Inc. +# +# SPDX-License-Identifier: Apache-2.0 +# + +import pbr.version + +DBSYNC_VENDOR = "Wind River Systems" +DBSYNC_PRODUCT = "Distributed Cloud DBsync Agent" +DBSYNC_PACKAGE = None # OS distro package version suffix + +version_info = pbr.version.VersionInfo('distributedcloud') +version_string = version_info.version_string + + +def vendor_string(): + return DBSYNC_VENDOR + + +def product_string(): + return DBSYNC_PRODUCT + + +def package_string(): + return DBSYNC_PACKAGE + + +def version_string_with_package(): + if package_string() is None: + return version_info.version_string() + else: + return "%s-%s" % (version_info.version_string(), package_string()) diff --git a/dcdbsync/config-generator.conf b/dcdbsync/config-generator.conf new file mode 100644 index 000000000..10b1a8e2f --- /dev/null +++ b/dcdbsync/config-generator.conf @@ -0,0 +1,14 @@ +[DEFAULT] +output_file = etc/dcdbsync/dcdbsync.conf.sample +wrap_width = 79 +namespace = dcdbsync.common.config +namespace = dcdbsync.api.api_config +namespace = keystonemiddleware.auth_token +namespace = oslo.messaging +namespace = oslo.middleware +namespace = oslo.db +namespace = oslo.log +namespace = oslo.policy +namespace = oslo.service.service +namespace = oslo.service.periodic_task +namespace = oslo.service.sslutils diff --git a/dcdbsync/db/__init__.py b/dcdbsync/db/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/dcdbsync/db/identity/__init__.py b/dcdbsync/db/identity/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/dcdbsync/db/identity/api.py b/dcdbsync/db/identity/api.py new file mode 100644 index 000000000..55ebc44be --- /dev/null +++ b/dcdbsync/db/identity/api.py @@ -0,0 +1,157 @@ +# Copyright (c) 2015 Ericsson AB. +# All Rights Reserved. +# +# 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. +# +# Copyright (c) 2019 Wind River Systems, Inc. +# +# SPDX-License-Identifier: Apache-2.0 +# + +''' +Interface for database access. + +SQLAlchemy is currently the only supported backend. +''' + +from oslo_config import cfg +from oslo_db import api + + +CONF = cfg.CONF + +_BACKEND_MAPPING = {'sqlalchemy': 'dcdbsync.db.identity.sqlalchemy.api'} + +IMPL = api.DBAPI.from_config(CONF, backend_mapping=_BACKEND_MAPPING) + + +def get_engine(): + return IMPL.get_engine() + + +def get_session(): + return IMPL.get_session() + + +################### + +# user db methods + +################### + +def user_get_all(context): + """Retrieve all users.""" + return IMPL.user_get_all(context) + + +def user_get(context, user_id): + """Retrieve details of a user.""" + return IMPL.user_get(context, user_id) + + +def user_create(context, payload): + """Create a user.""" + return IMPL.user_create(context, payload) + + +def user_update(context, user_ref, payload): + """Update a user""" + return IMPL.user_update(context, user_ref, payload) + + +################### + +# project db methods + +################### + +def project_get_all(context): + """Retrieve all projects.""" + return IMPL.project_get_all(context) + + +def project_get(context, project_id): + """Retrieve details of a project.""" + return IMPL.project_get(context, project_id) + + +def project_create(context, payload): + """Create a project.""" + return IMPL.project_create(context, payload) + + +def project_update(context, project_ref, payload): + """Update a project""" + return IMPL.project_update(context, project_ref, payload) + + +################### + +# role db methods + +################### + +def role_get_all(context): + """Retrieve all roles.""" + return IMPL.role_get_all(context) + + +def role_get(context, role_id): + """Retrieve details of a role.""" + return IMPL.role_get(context, role_id) + + +def role_create(context, payload): + """Create a role.""" + return IMPL.role_create(context, payload) + + +def role_update(context, role_ref, payload): + """Update a role""" + return IMPL.role_update(context, role_ref, payload) + + +################### + +# revoke_event db methods + +################### + +def revoke_event_get_all(context): + """Retrieve all token revocation events.""" + return IMPL.revoke_event_get_all(context) + + +def revoke_event_get_by_audit(context, audit_id): + """Retrieve details of a token revocation event.""" + return IMPL.revoke_event_get_by_audit(context, audit_id) + + +def revoke_event_get_by_user(context, user_id, issued_before): + """Retrieve details of a token revocation event.""" + return IMPL.revoke_event_get_by_user(context, user_id, issued_before) + + +def revoke_event_create(context, payload): + """Create a token revocation event.""" + return IMPL.revoke_event_create(context, payload) + + +def revoke_event_delete_by_audit(context, audit_id): + """Delete a token revocation event.""" + return IMPL.revoke_event_delete_by_audit(context, audit_id) + + +def revoke_event_delete_by_user(context, user_id, issued_before): + """Delete a token revocation event.""" + return IMPL.revoke_event_delete_by_user(context, user_id, issued_before) diff --git a/dcdbsync/db/identity/sqlalchemy/__init__.py b/dcdbsync/db/identity/sqlalchemy/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/dcdbsync/db/identity/sqlalchemy/api.py b/dcdbsync/db/identity/sqlalchemy/api.py new file mode 100644 index 000000000..64157fe7f --- /dev/null +++ b/dcdbsync/db/identity/sqlalchemy/api.py @@ -0,0 +1,538 @@ +# Copyright (c) 2015 Ericsson AB. +# All Rights Reserved. +# +# 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. +# +# Copyright (c) 2019 Wind River Systems, Inc. +# +# SPDX-License-Identifier: Apache-2.0 +# + +""" +Implementation of SQLAlchemy backend. +""" + +import sys +import threading + +from oslo_db.sqlalchemy import enginefacade +from oslo_log import log as logging + +from sqlalchemy import Table, MetaData +from sqlalchemy.sql import select + +from dcdbsync.common import exceptions as exception +from dcdbsync.common.i18n import _ + +LOG = logging.getLogger(__name__) + + +_CONTEXT = threading.local() + + +class TableRegistry(object): + def __init__(self): + self.metadata = MetaData() + + def get(self, connection, tablename): + try: + table = self.metadata.tables[tablename] + except KeyError: + table = Table( + tablename, + self.metadata, + autoload_with=connection + ) + return table + +registry = TableRegistry() + + +def get_read_connection(): + return enginefacade.reader.connection.using(_CONTEXT) + + +def get_write_connection(): + return enginefacade.writer.connection.using(_CONTEXT) + + +def row2dict(table, row): + d = {} + for c in table.columns: + c_value = getattr(row, c.name) + d[c.name] = c_value + + return d + + +def index2column(r_table, index_name): + column = None + for c in r_table.columns: + if c.name == index_name: + column = c + break + + return column + + +def query(connection, table, index_name=None, index_value=None): + global registry + r_table = registry.get(connection, table) + + if index_name and index_value: + c = index2column(r_table, index_name) + stmt = select([r_table]).where(c == index_value) + else: + stmt = select([r_table]) + + records = [] + result = connection.execute(stmt) + for row in result: + # convert the row into a dictionary + d = row2dict(r_table, row) + records.append(d) + + return records + + +def insert(connection, table, data): + global registry + r_table = registry.get(connection, table) + stmt = r_table.insert() + + connection.execute(stmt, data) + + +def delete(connection, table, index_name, index_value): + global registry + r_table = registry.get(connection, table) + + c = index2column(r_table, index_name) + stmt = r_table.delete().where(c == index_value) + connection.execute(stmt) + + +def update(connection, table, index_name, index_value, data): + global registry + r_table = registry.get(connection, table) + + c = index2column(r_table, index_name) + stmt = r_table.update().where(c == index_value).values(data) + connection.execute(stmt) + + +def get_backend(): + """The backend is this module itself.""" + return sys.modules[__name__] + + +def is_admin_context(context): + """Indicate if the request context is an administrator.""" + if not context: + LOG.warning(_('Use of empty request context is deprecated'), + DeprecationWarning) + raise Exception('die') + return context.is_admin + + +def is_user_context(context): + """Indicate if the request context is a normal user.""" + if not context: + return False + if context.is_admin: + return False + if not context.user or not context.project: + return False + return True + + +def require_admin_context(f): + """Decorator to require admin request context. + + The first argument to the wrapped function must be the context. + """ + def wrapper(*args, **kwargs): + if not is_admin_context(args[0]): + raise exception.AdminRequired() + return f(*args, **kwargs) + + return wrapper + + +def require_context(f): + """Decorator to require *any* user or admin context. + + This does no authorization for user or project access matching, see + :py:func:`authorize_project_context` and + :py:func:`authorize_user_context`. + The first argument to the wrapped function must be the context. + + """ + def wrapper(*args, **kwargs): + if not is_admin_context(args[0]) and not is_user_context(args[0]): + raise exception.NotAuthorized() + return f(*args, **kwargs) + + return wrapper + + +################### + +# identity users + +################### + +@require_context +def user_get_all(context): + result = [] + + with get_read_connection() as conn: + # user table + users = query(conn, 'user') + # local_user table + local_users = query(conn, 'local_user') + # password table + passwords = query(conn, 'password') + + for local_user in local_users: + user = {'user': user for user in users if user['id'] + == local_user['user_id']} + user_passwords = {'password': [password for password in passwords + if password['local_user_id'] == + local_user['id']]} + user_consolidated = dict({'local_user': local_user}.items() + + user.items() + user_passwords.items()) + result.append(user_consolidated) + + return result + + +@require_context +def user_get(context, user_id): + result = {} + + with get_read_connection() as conn: + # user table + users = query(conn, 'user', 'id', user_id) + if not users: + raise exception.UserNotFound(user_id=user_id) + result['user'] = users[0] + # local_user table + local_users = query(conn, 'local_user', 'user_id', user_id) + if not local_users: + raise exception.UserNotFound(user_id=user_id) + result['local_user'] = local_users[0] + # password table + result['password'] = [] + if result['local_user']: + result['password'] = query(conn, 'password', + 'local_user_id', + result['local_user'].get('id')) + + return result + + +@require_admin_context +def user_create(context, payload): + users = [payload['user']] + local_users = [payload['local_user']] + passwords = payload['password'] + + with get_write_connection() as conn: + insert(conn, 'user', users) + + # ignore auto generated id + for local_user in local_users: + local_user.pop('id', None) + insert(conn, 'local_user', local_users) + + inserted_local_users = query(conn, 'local_user', 'user_id', + payload['local_user']['user_id']) + + if not inserted_local_users: + raise exception.UserNotFound(user_id=payload['local_user'] + ['user_id']) + + for password in passwords: + # ignore auto generated id + password.pop('id', None) + password['local_user_id'] = inserted_local_users[0]['id'] + + insert(conn, 'password', passwords) + + return user_get(context, payload['user']['id']) + + +@require_admin_context +def user_update(context, user_id, payload): + with get_write_connection() as conn: + # user table + table = 'user' + new_user_id = user_id + if table in payload: + user = payload[table] + update(conn, table, 'id', user_id, user) + new_user_id = user.get('id') + # local_user table + table = 'local_user' + if table in payload: + local_user = payload[table] + # ignore auto generated id + local_user.pop('id', None) + update(conn, table, 'user_id', user_id, local_user) + updated_local_users = query(conn, table, 'user_id', + new_user_id) + + if not updated_local_users: + raise exception.UserNotFound(user_id=payload[table]['user_id']) + # password table + table = 'password' + if table in payload: + delete(conn, table, 'local_user_id', + updated_local_users[0]['id']) + passwords = payload[table] + for password in passwords: + # ignore auto generated ids + password.pop('id', None) + password['local_user_id'] = \ + updated_local_users[0]['id'] + insert(conn, table, password) + # Need to update the actor_id in assignment table + # if the user id is updated + if user_id != new_user_id: + table = 'assignment' + assignment = {'actor_id': new_user_id} + update(conn, table, 'actor_id', user_id, assignment) + + return user_get(context, new_user_id) + + +################### + +# identity projects + +################### + +@require_context +def project_get_all(context): + result = [] + + with get_read_connection() as conn: + # project table + projects = query(conn, 'project') + + for project in projects: + project_consolidated = {'project': project} + result.append(project_consolidated) + + return result + + +@require_context +def project_get(context, project_id): + result = {} + + with get_read_connection() as conn: + # project table + projects = query(conn, 'project', 'id', project_id) + if not projects: + raise exception.ProjectNotFound(project_id=project_id) + result['project'] = projects[0] + + return result + + +@require_admin_context +def project_create(context, payload): + projects = [payload['project']] + + with get_write_connection() as conn: + insert(conn, 'project', projects) + + return project_get(context, payload['project']['id']) + + +@require_admin_context +def project_update(context, project_id, payload): + with get_write_connection() as conn: + # project table + table = 'project' + new_project_id = project_id + if table in payload: + project = payload[table] + update(conn, table, 'id', project_id, project) + new_project_id = project.get('id') + + # Need to update the target_id in assignment table + # if the project id is updated + if project_id != new_project_id: + table = 'assignment' + assignment = {'target_id': new_project_id} + update(conn, table, 'target_id', project_id, assignment) + + return project_get(context, new_project_id) + + +################### + +# identity roles + +################### + +@require_context +def role_get_all(context): + result = [] + + with get_read_connection() as conn: + # role table + roles = query(conn, 'role') + + for role in roles: + role_consolidated = {'role': role} + result.append(role_consolidated) + + return result + + +@require_context +def role_get(context, role_id): + result = {} + + with get_read_connection() as conn: + # role table + roles = query(conn, 'role', 'id', role_id) + if not roles: + raise exception.RoleNotFound(role_id=role_id) + result['role'] = roles[0] + + return result + + +@require_admin_context +def role_create(context, payload): + roles = [payload['role']] + + with get_write_connection() as conn: + insert(conn, 'role', roles) + + return role_get(context, payload['role']['id']) + + +@require_admin_context +def role_update(context, role_id, payload): + with get_write_connection() as conn: + # role table + table = 'role' + new_role_id = role_id + if table in payload: + role = payload[table] + update(conn, table, 'id', role_id, role) + new_role_id = role.get('id') + + # Need to update the role_id in assignment table + # if the role id is updated + if role_id != new_role_id: + table = 'assignment' + assignment = {'role_id': new_role_id} + update(conn, table, 'role_id', role_id, assignment) + + return role_get(context, new_role_id) + + +################################## + +# identity token revocation events + +################################## + +@require_context +def revoke_event_get_all(context): + result = [] + + with get_read_connection() as conn: + # revocation_event table + revoke_events = query(conn, 'revocation_event') + + for revoke_event in revoke_events: + revoke_event_consolidated = {'revocation_event': revoke_event} + result.append(revoke_event_consolidated) + + return result + + +@require_context +def revoke_event_get_by_audit(context, audit_id): + result = {} + + with get_read_connection() as conn: + # revocation_event table + revoke_events = query(conn, 'revocation_event', 'audit_id', + audit_id) + if not revoke_events: + raise exception.RevokeEventNotFound() + result['revocation_event'] = revoke_events[0] + + return result + + +@require_context +def revoke_event_get_by_user(context, user_id, issued_before): + result = {} + + with get_read_connection() as conn: + # revocation_event table + events = query(conn, 'revocation_event', 'user_id', user_id) + revoke_events = [event for event in events if + str(event['issued_before']) == issued_before] + if not revoke_events: + raise exception.RevokeEventNotFound() + result['revocation_event'] = revoke_events[0] + + return result + + +@require_admin_context +def revoke_event_create(context, payload): + revoke_event = payload['revocation_event'] + # ignore auto generated id + revoke_event.pop('id', None) + + revoke_events = [revoke_event] + + with get_write_connection() as conn: + insert(conn, 'revocation_event', revoke_events) + + result = {} + if revoke_event.get('audit_id') is not None: + result = revoke_event_get_by_audit(context, + revoke_event.get('audit_id')) + elif (revoke_event.get('user_id') is not None) and \ + (revoke_event.get('issued_before') is not None): + result = revoke_event_get_by_user(context, + revoke_event.get('user_id'), + revoke_event.get('issued_before')) + return result + + +@require_admin_context +def revoke_event_delete_by_audit(context, audit_id): + with get_write_connection() as conn: + delete(conn, 'revocation_event', 'audit_id', audit_id) + + +@require_admin_context +def revoke_event_delete_by_user(context, user_id, issued_before): + with get_write_connection() as conn: + result = revoke_event_get_by_user(context, user_id, issued_before) + event_id = result['revocation_event']['id'] + delete(conn, 'revocation_event', 'id', event_id) diff --git a/dcdbsync/db/identity/utils.py b/dcdbsync/db/identity/utils.py new file mode 100644 index 000000000..e437a90d9 --- /dev/null +++ b/dcdbsync/db/identity/utils.py @@ -0,0 +1,53 @@ +# Copyright (c) 2015 Ericsson AB. +# All Rights Reserved. +# +# 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. +# +# Copyright (c) 2019 Wind River Systems, Inc. +# +# SPDX-License-Identifier: Apache-2.0 +# + + +class LazyPluggable(object): + """A pluggable backend loaded lazily based on some value.""" + + def __init__(self, pivot, **backends): + self.__backends = backends + self.__pivot = pivot + self.__backend = None + + def __get_backend(self): + if not self.__backend: + backend_name = 'sqlalchemy' + backend = self.__backends[backend_name] + if isinstance(backend, tuple): + name = backend[0] + fromlist = backend[1] + else: + name = backend + fromlist = backend + + self.__backend = __import__(name, None, None, fromlist) + return self.__backend + + def __getattr__(self, key): + backend = self.__get_backend() + return getattr(backend, key) + + +IMPL = LazyPluggable('backend', sqlalchemy='dcdbsync.db.sqlalchemy.api') + + +def purge_deleted(age, granularity='days'): + IMPL.purge_deleted(age, granularity) diff --git a/dcdbsync/dbsyncclient/__init__.py b/dcdbsync/dbsyncclient/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/dcdbsync/dbsyncclient/base.py b/dcdbsync/dbsyncclient/base.py new file mode 100644 index 000000000..fc8d04a56 --- /dev/null +++ b/dcdbsync/dbsyncclient/base.py @@ -0,0 +1,110 @@ +# Copyright (c) 2016 Ericsson AB +# +# 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. +# +# Copyright (c) 2019 Wind River Systems, Inc. +# +# SPDX-License-Identifier: Apache-2.0 +# + +from bs4 import BeautifulSoup +import json + +from dcdbsync.dbsyncclient import exceptions + + +class Resource(object): + # This will be overridden by the actual resource + resource_name = 'Something' + + +class ResourceManager(object): + resource_class = None + + def __init__(self, http_client): + self.http_client = http_client + + def _generate_resource(self, json_response_key): + json_objects = [json_response_key[item] for item in json_response_key] + resource = [] + for json_object in json_objects: + for resource_data in json_object: + resource.append(self.resource_class(self, resource_data, + json_object[resource_data])) + return resource + + def _list(self, url, response_key=None): + resp = self.http_client.get(url) + if resp.status_code != 200: + self._raise_api_exception(resp) + json_response_key = get_json(resp) + resource = self._generate_resource(json_response_key) + return resource + + def _update(self, url, data): + data = json.dumps(data) + resp = self.http_client.put(url, data) + if resp.status_code != 200: + self._raise_api_exception(resp) + json_response_key = get_json(resp) + result = self._generate_resource(json_response_key) + return result + + def _sync(self, url, data=None): + resp = self.http_client.put(url, data) + if resp.status_code != 200: + self._raise_api_exception(resp) + + def _detail(self, url): + resp = self.http_client.get(url) + if resp.status_code != 200: + self._raise_api_exception(resp) + json_response_key = get_json(resp) + json_objects = [json_response_key[item] for item in json_response_key] + resource = [] + for json_object in json_objects: + data = json_object.get('usage').keys() + for values in data: + resource.append(self.resource_class(self, values, + json_object['limits'][values], + json_object['usage'][values])) + return resource + + def _delete(self, url): + resp = self.http_client.delete(url) + if resp.status_code != 200: + self._raise_api_exception(resp) + + def _raise_api_exception(self, resp): + error_html = resp.content + soup = BeautifulSoup(error_html, 'html.parser') + # Get the raw html with get_text, strip out the blank lines on + # front and back, then get rid of the 2 lines of error code number + # and error code explanation so that we are left with just the + # meaningful error text. + try: + error_msg = soup.body.get_text().lstrip().rstrip().split('\n')[2] + except Exception: + error_msg = resp.content + + raise exceptions.APIException(error_code=resp.status_code, + error_message=error_msg) + + +def get_json(response): + """Get JSON representation of response.""" + json_field_or_function = getattr(response, 'json', None) + if callable(json_field_or_function): + return response.json() + else: + return json.loads(response.content) diff --git a/dcdbsync/dbsyncclient/client.py b/dcdbsync/dbsyncclient/client.py new file mode 100644 index 000000000..87729c911 --- /dev/null +++ b/dcdbsync/dbsyncclient/client.py @@ -0,0 +1,60 @@ +# Copyright 2016 - Ericsson AB +# +# 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. +# +# Copyright (c) 2019 Wind River Systems, Inc. +# +# SPDX-License-Identifier: Apache-2.0 +# + +import six + +from dcdbsync.dbsyncclient.v1 import client as client_v1 + + +def Client(dbsync_agent_url=None, username=None, api_key=None, + project_name=None, auth_url=None, project_id=None, + endpoint_type='publicURL', service_type='dcorch-dbsync', + auth_token=None, user_id=None, cacert=None, insecure=False, + profile=None, auth_type='keystone', client_id=None, + client_secret=None, session=None, **kwargs): + if dbsync_agent_url and not isinstance(dbsync_agent_url, six.string_types): + raise RuntimeError('DC DBsync agent url should be a string.') + + return client_v1.Client( + dbsync_agent_url=dbsync_agent_url, + username=username, + api_key=api_key, + project_name=project_name, + auth_url=auth_url, + project_id=project_id, + endpoint_type=endpoint_type, + service_type=service_type, + auth_token=auth_token, + user_id=user_id, + cacert=cacert, + insecure=insecure, + profile=profile, + auth_type=auth_type, + client_id=client_id, + client_secret=client_secret, + session=session, + **kwargs + ) + + +def determine_client_version(dbsync_version): + if dbsync_version.find("v1.0") != -1: + return 1 + + raise RuntimeError("Cannot determine DC DBsync agent API version") diff --git a/dcdbsync/dbsyncclient/exceptions.py b/dcdbsync/dbsyncclient/exceptions.py new file mode 100644 index 000000000..05165ad72 --- /dev/null +++ b/dcdbsync/dbsyncclient/exceptions.py @@ -0,0 +1,106 @@ +# Copyright 2016 Ericsson AB +# +# 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. +# +# Copyright (c) 2019 Wind River Systems, Inc. +# +# SPDX-License-Identifier: Apache-2.0 +# + + +class DBsyncClientException(Exception): + """Base Exception for DB sync client + + To correctly use this class, inherit from it and define + a 'message' and 'code' properties. + """ + message = "An unknown exception occurred" + code = "UNKNOWN_EXCEPTION" + + def __str__(self): + return self.message + + def __init__(self, message=message): + self.message = message + super(DBsyncClientException, self).__init__( + '%s: %s' % (self.code, self.message)) + + +class IllegalArgumentException(DBsyncClientException): + message = "IllegalArgumentException occurred" + code = "ILLEGAL_ARGUMENT_EXCEPTION" + + def __init__(self, message=None): + if message: + self.message = message + + +class CommandError(DBsyncClientException): + message = "CommandErrorException occurred" + code = "COMMAND_ERROR_EXCEPTION" + + def __init__(self, message=None): + if message: + self.message = message + + +class ConnectTimeout(DBsyncClientException): + message = "ConnectTimeOutException occurred" + code = "CONNECT_TIMEOUT_EXCEPTION" + + def __init__(self, message=None): + if message: + self.message = message + + +class ConnectFailure(DBsyncClientException): + message = "ConnectFailureException occurred" + code = "CONNECT_FAILURE_EXCEPTION" + + def __init__(self, message=None): + if message: + self.message = message + + +class UnknownConnectionError(DBsyncClientException): + message = "UnknownConnectionErrorException occurred" + code = "UNKNOWN_CONNECTION_ERROR_EXCEPTION" + + def __init__(self, message=None): + if message: + self.message = message + + +class Unauthorized(DBsyncClientException): + message = "UnauthorizedException occurred" + code = "UNAUTHORIZED_EXCEPTION" + + def __init__(self, message=None): + if message: + self.message = message + + +class NotFound(DBsyncClientException): + message = "NotFoundException occurred" + code = "NOTFOUND_EXCEPTION" + + def __init__(self, message=None): + if message: + self.message = message + + +class APIException(Exception): + def __init__(self, error_code=None, error_message=None): + super(APIException, self).__init__(error_message) + self.error_code = error_code + self.error_message = error_message diff --git a/dcdbsync/dbsyncclient/httpclient.py b/dcdbsync/dbsyncclient/httpclient.py new file mode 100644 index 000000000..744b9be96 --- /dev/null +++ b/dcdbsync/dbsyncclient/httpclient.py @@ -0,0 +1,184 @@ +# Copyright 2013 - Mirantis, Inc. +# Copyright 2016 - StackStorm, Inc. +# Copyright 2016 - Ericsson AB. +# +# 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. +# +# Copyright (c) 2019 Wind River Systems, Inc. +# +# SPDX-License-Identifier: Apache-2.0 +# + +import copy +import os + +import requests + +import logging + +from dcdbsync.dbsyncclient import exceptions +from oslo_utils import importutils +osprofiler_web = importutils.try_import("osprofiler.web") + +LOG = logging.getLogger(__name__) + + +def log_request(func): + def decorator(self, *args, **kwargs): + resp = func(self, *args, **kwargs) + LOG.debug("HTTP %s %s %d" % (resp.request.method, resp.url, + resp.status_code)) + return resp + + return decorator + + +class HTTPClient(object): + def __init__(self, base_url, token=None, project_id=None, user_id=None, + cacert=None, insecure=False): + self.base_url = base_url + self.token = token + self.project_id = project_id + self.user_id = user_id + self.ssl_options = {} + + if self.base_url.startswith('https'): + if cacert and not os.path.exists(cacert): + raise ValueError('Unable to locate cacert file ' + 'at %s.' % cacert) + + if cacert and insecure: + LOG.warning('Client is set to not verify even though ' + 'cacert is provided.') + + self.ssl_options['verify'] = not insecure + self.ssl_options['cert'] = cacert + + @log_request + def get(self, url, headers=None): + options = self._get_request_options('get', headers) + + try: + url = self.base_url + url + return requests.get(url, **options) + except requests.exceptions.Timeout: + msg = 'Request to %s timed out' % url + raise exceptions.ConnectTimeout(msg) + except requests.exceptions.ConnectionError as e: + msg = 'Unable to establish connection to %s: %s' % (url, e) + raise exceptions.ConnectFailure(msg) + except requests.exceptions.RequestException as e: + msg = 'Unexpected exception for %s: %s' % (url, e) + raise exceptions.UnknownConnectionError(msg, e) + + @log_request + def post(self, url, body, headers=None): + options = self._get_request_options('post', headers) + + try: + url = self.base_url + url + return requests.post(url, body, **options) + except requests.exceptions.Timeout: + msg = 'Request to %s timed out' % url + raise exceptions.ConnectTimeout(msg) + except requests.exceptions.ConnectionError as e: + msg = 'Unable to establish connection to %s: %s' % (url, e) + raise exceptions.ConnectFailure(msg) + except requests.exceptions.RequestException as e: + msg = 'Unexpected exception for %s: %s' % (url, e) + raise exceptions.UnknownConnectionError(msg, e) + + @log_request + def put(self, url, body, headers=None): + options = self._get_request_options('put', headers) + + try: + url = self.base_url + url + return requests.put(url, body, **options) + except requests.exceptions.Timeout: + msg = 'Request to %s timed out' % url + raise exceptions.ConnectTimeout(msg) + except requests.exceptions.ConnectionError as e: + msg = 'Unable to establish connection to %s: %s' % (url, e) + raise exceptions.ConnectFailure(msg) + except requests.exceptions.RequestException as e: + msg = 'Unexpected exception for %s: %s' % (url, e) + raise exceptions.UnknownConnectionError(msg, e) + + @log_request + def patch(self, url, body, headers=None): + options = self._get_request_options('patch', headers) + + try: + url = self.base_url + url + return requests.patch(url, body, **options) + except requests.exceptions.Timeout: + msg = 'Request to %s timed out' % url + raise exceptions.ConnectTimeout(msg) + except requests.exceptions.ConnectionError as e: + msg = 'Unable to establish connection to %s: %s' % (url, e) + raise exceptions.ConnectFailure(msg) + except requests.exceptions.RequestException as e: + msg = 'Unexpected exception for %s: %s' % (url, e) + raise exceptions.UnknownConnectionError(msg, e) + + @log_request + def delete(self, url, headers=None): + options = self._get_request_options('delete', headers) + + try: + url = self.base_url + url + return requests.delete(url, **options) + except requests.exceptions.Timeout: + msg = 'Request to %s timed out' % url + raise exceptions.ConnectTimeout(msg) + except requests.exceptions.ConnectionError as e: + msg = 'Unable to establish connection to %s: %s' % (url, e) + raise exceptions.ConnectFailure(msg) + except requests.exceptions.RequestException as e: + msg = 'Unexpected exception for %s: %s' % (url, e) + raise exceptions.UnknownConnectionError(msg, e) + + def _get_request_options(self, method, headers): + headers = self._update_headers(headers) + + if method in ['post', 'put', 'patch']: + content_type = headers.get('content-type', 'application/json') + headers['content-type'] = content_type + + options = copy.deepcopy(self.ssl_options) + options['headers'] = headers + + return options + + def _update_headers(self, headers): + if not headers: + headers = {} + + token = headers.get('x-auth-token', self.token) + if token: + headers['x-auth-token'] = token + + project_id = headers.get('X-Project-Id', self.project_id) + if project_id: + headers['X-Project-Id'] = project_id + + user_id = headers.get('X-User-Id', self.user_id) + if user_id: + headers['X-User-Id'] = user_id + + # Add headers for osprofiler. + if osprofiler_web: + headers.update(osprofiler_web.get_trace_id_headers()) + + return headers diff --git a/dcdbsync/dbsyncclient/v1/__init__.py b/dcdbsync/dbsyncclient/v1/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/dcdbsync/dbsyncclient/v1/client.py b/dcdbsync/dbsyncclient/v1/client.py new file mode 100644 index 000000000..7966efee3 --- /dev/null +++ b/dcdbsync/dbsyncclient/v1/client.py @@ -0,0 +1,178 @@ +# Copyright 2014 - Mirantis, Inc. +# Copyright 2015 - StackStorm, Inc. +# Copyright 2016 - Ericsson AB. +# +# 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. +# +# Copyright (c) 2019 Wind River Systems, Inc. +# +# SPDX-License-Identifier: Apache-2.0 +# + +import keystoneauth1.identity.generic as auth_plugin +from keystoneauth1 import session as ks_session + +from dcdbsync.dbsyncclient import httpclient +from dcdbsync.dbsyncclient.v1.identity import identity_manager as im +from dcdbsync.dbsyncclient.v1.identity import project_manager as pm +from dcdbsync.dbsyncclient.v1.identity import role_manager as rm +from dcdbsync.dbsyncclient.v1.identity \ + import token_revoke_event_manager as trem + +from oslo_utils import importutils +osprofiler_profiler = importutils.try_import("osprofiler.profiler") + +import six + + +_DEFAULT_DBSYNC_AGENT_URL = "http://localhost:8219/v1.0" + + +class Client(object): + """Class where the communication from KB to Keystone happens.""" + + def __init__(self, dbsync_agent_url=None, username=None, api_key=None, + project_name=None, auth_url=None, project_id=None, + endpoint_type='publicURL', service_type='dcorch-dbsync', + auth_token=None, user_id=None, cacert=None, insecure=False, + profile=None, auth_type='keystone', client_id=None, + client_secret=None, session=None, **kwargs): + """Communicates with Keystone to fetch necessary values.""" + if dbsync_agent_url and not isinstance(dbsync_agent_url, + six.string_types): + raise RuntimeError('DC DBsync agent url should be a string.') + + if auth_url or session: + if auth_type == 'keystone': + (dbsync_agent_url, auth_token, project_id, user_id) = ( + authenticate( + dbsync_agent_url, + username, + api_key, + project_name, + auth_url, + project_id, + endpoint_type, + service_type, + auth_token, + user_id, + session, + cacert, + insecure, + **kwargs + ) + ) + else: + raise RuntimeError( + 'Invalid authentication type [value=%s, valid_values=%s]' + % (auth_type, 'keystone') + ) + + if not dbsync_agent_url: + dbsync_agent_url = _DEFAULT_DBSYNC_AGENT_URL + + if osprofiler_profiler and profile: + osprofiler_profiler.init(profile) + + self.http_client = httpclient.HTTPClient( + dbsync_agent_url, + auth_token, + project_id, + user_id, + cacert=cacert, + insecure=insecure + ) + + # Create all managers + self.identity_manager = im.identity_manager(self.http_client) + self.project_manager = pm.project_manager(self.http_client) + self.role_manager = rm.role_manager(self.http_client) + self.revoke_event_manager = trem.revoke_event_manager(self.http_client) + + # update to get a new token + def update(self, session=None): + if session: + (dbsync_agent_url, auth_token, project_id, user_id) = ( + authenticate( + auth_url=session.auth.auth_url, + username=session.auth._username, + api_key=session.auth._password, + project_name=session.auth._project_name, + user_domain_name=session.auth._user_domain_name, + project_domain_name=session.auth._project_domain_name, + ) + ) + + self.http_client.token = auth_token + + +def authenticate(dbsync_agent_url=None, username=None, + api_key=None, project_name=None, auth_url=None, + project_id=None, endpoint_type='publicURL', + service_type='dcorch-dbsync', auth_token=None, user_id=None, + session=None, cacert=None, insecure=False, **kwargs): + """Get token, project_id, user_id and Endpoint.""" + if project_name and project_id: + raise RuntimeError( + 'Only project name or project id should be set' + ) + + if username and user_id: + raise RuntimeError( + 'Only user name or user id should be set' + ) + user_domain_name = kwargs.get('user_domain_name') + user_domain_id = kwargs.get('user_domain_id') + project_domain_name = kwargs.get('project_domain_name') + project_domain_id = kwargs.get('project_domain_id') + + if session is None: + if auth_token: + auth = auth_plugin.Token( + auth_url=auth_url, + token=auth_token, + project_id=project_id, + project_name=project_name, + project_domain_name=project_domain_name, + project_domain_id=project_domain_id, + ) + + elif api_key and (username or user_id): + auth = auth_plugin.Password( + auth_url=auth_url, + username=username, + user_id=user_id, + password=api_key, + project_id=project_id, + project_name=project_name, + user_domain_name=user_domain_name, + user_domain_id=user_domain_id, + project_domain_name=project_domain_name, + project_domain_id=project_domain_id) + + else: + raise RuntimeError('You must either provide a valid token or' + 'a password (api_key) and a user.') + if auth: + session = ks_session.Session(auth=auth) + + if session: + token = session.get_token() + project_id = session.get_project_id() + user_id = session.get_user_id() + if not dbsync_agent_url: + dbsync_agent_url = session.get_endpoint( + service_type=service_type, + interface=endpoint_type) + + return dbsync_agent_url, token, project_id, user_id diff --git a/dcdbsync/dbsyncclient/v1/identity/__init__.py b/dcdbsync/dbsyncclient/v1/identity/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/dcdbsync/dbsyncclient/v1/identity/identity_manager.py b/dcdbsync/dbsyncclient/v1/identity/identity_manager.py new file mode 100644 index 000000000..eaf20f539 --- /dev/null +++ b/dcdbsync/dbsyncclient/v1/identity/identity_manager.py @@ -0,0 +1,202 @@ +# Copyright (c) 2017 Ericsson AB. +# All Rights Reserved. +# +# 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. +# +# Copyright (c) 2019 Wind River Systems, Inc. +# +# SPDX-License-Identifier: Apache-2.0 +# + + +from dcdbsync.dbsyncclient import base +from dcdbsync.dbsyncclient.base import get_json +from dcdbsync.dbsyncclient import exceptions + + +class Password(base.Resource): + resource_name = 'password' + + def __init__(self, manager, id, local_user_id, password, self_service, + password_hash, created_at, created_at_int, expires_at, + expires_at_int): + self.manager = manager + self.id = id + # Foreign key to local_user.id + self.local_user_id = local_user_id + self.password = password + self.self_service = self_service + self.password_hash = password_hash + self.created_at = created_at + self.created_at_int = created_at_int + self.expires_at = expires_at + self.expires_at_int = expires_at_int + + +class LocalUser(base.Resource): + resource_name = 'localUser' + + def __init__(self, manager, id, domain_id, name, user_id, + failed_auth_count, failed_auth_at, + passwords=[]): + self.manager = manager + self.id = id + self.domain_id = domain_id + self.name = name + self.user_id = user_id + self.failed_auth_count = failed_auth_count + self.failed_auth_at = failed_auth_at + self.passwords = passwords + + +class User(base.Resource): + resource_name = 'user' + + def __init__(self, manager, id, domain_id, default_project_id, + enabled, created_at, last_active_at, local_user, + extra={}): + self.manager = manager + self.id = id + self.domain_id = domain_id + self.default_project_id = default_project_id + self.enabled = enabled + self.created_at = created_at + self.last_active_at = last_active_at + self.extra = extra + self.local_user = local_user + + def info(self): + resource_info = dict() + resource_info.update({self.resource_name: + {'name': self.local_user.name, + 'id': self.id, + 'domain_id': self.domain_id}}) + + return resource_info + + +class identity_manager(base.ResourceManager): + resource_class = User + + def user_create(self, url, data): + resp = self.http_client.post(url, data) + + # Unauthorized request + if resp.status_code == 401: + raise exceptions.Unauthorized('Unauthorized request.') + if resp.status_code != 201: + self._raise_api_exception(resp) + + # Converted into python dict + json_object = get_json(resp) + return json_object + + def users_list(self, url): + resp = self.http_client.get(url) + + # Unauthorized request + if resp.status_code == 401: + raise exceptions.Unauthorized('Unauthorized request.') + if resp.status_code != 200: + self._raise_api_exception(resp) + + # Converted into python dict + json_objects = get_json(resp) + + users = [] + for json_object in json_objects: + passwords = [] + for object in json_object['password']: + # skip empty password + if not object: + continue + password = Password( + self, + id=object['id'], + local_user_id=object['local_user_id'], + password=object['password'], + self_service=object['self_service'], + password_hash=object['password_hash'], + created_at=object['created_at'], + created_at_int=object['created_at_int'], + expires_at=object['expires_at'], + expires_at_int=object['expires_at_int']) + passwords.append(password) + + local_user = LocalUser( + self, + id=json_object['local_user']['id'], + domain_id=json_object['local_user']['domain_id'], + name=json_object['local_user']['name'], + user_id=json_object['local_user']['user_id'], + failed_auth_count=json_object['local_user'][ + 'failed_auth_count'], + failed_auth_at=json_object['local_user']['failed_auth_at'], + passwords=passwords) + + user = User( + self, + id=json_object['user']['id'], + domain_id=json_object['user']['domain_id'], + default_project_id=json_object['user']['default_project_id'], + enabled=json_object['user']['enabled'], + created_at=json_object['user']['created_at'], + last_active_at=json_object['user']['last_active_at'], + extra=json_object['user']['extra'], + local_user=local_user) + + users.append(user) + + return users + + def _user_detail(self, url): + resp = self.http_client.get(url) + + # Unauthorized request + if resp.status_code == 401: + raise exceptions.Unauthorized('Unauthorized request.') + if resp.status_code != 200: + self._raise_api_exception(resp) + + # Return user details in original json format, + # ie, without convert it into python dict + return resp.content + + def _user_update(self, url, data): + resp = self.http_client.put(url, data) + + # Unauthorized request + if resp.status_code == 401: + raise exceptions.Unauthorized('Unauthorized request.') + if resp.status_code != 200: + self._raise_api_exception(resp) + + # Converted into python dict + json_object = get_json(resp) + return json_object + + def add_user(self, data): + url = '/identity/users/' + return self.user_create(url, data) + + def list_users(self): + url = '/identity/users/' + return self.users_list(url) + + def user_detail(self, user_ref): + url = '/identity/users/%s' % user_ref + return self._user_detail(url) + + def update_user(self, user_ref, data): + url = '/identity/users/%s' % user_ref + return self._user_update(url, data) diff --git a/dcdbsync/dbsyncclient/v1/identity/project_manager.py b/dcdbsync/dbsyncclient/v1/identity/project_manager.py new file mode 100644 index 000000000..75b0b6358 --- /dev/null +++ b/dcdbsync/dbsyncclient/v1/identity/project_manager.py @@ -0,0 +1,138 @@ +# Copyright (c) 2017 Ericsson AB. +# All Rights Reserved. +# +# 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. +# +# Copyright (c) 2019 Wind River Systems, Inc. +# +# SPDX-License-Identifier: Apache-2.0 +# + + +from dcdbsync.dbsyncclient import base +from dcdbsync.dbsyncclient.base import get_json +from dcdbsync.dbsyncclient import exceptions + + +class Project(base.Resource): + resource_name = 'project' + + def __init__(self, manager, id, domain_id, name, + enabled, parent_id, is_domain, extra={}, + description=""): + self.manager = manager + self.id = id + self.domain_id = domain_id + self.name = name + self.extra = extra + self.description = description + self.enabled = enabled + self.parent_id = parent_id + self.is_domain = is_domain + + def info(self): + resource_info = dict() + resource_info.update({self.resource_name: + {'name': self.name, + 'id': self.id, + 'domain_id': self.domain_id}}) + return resource_info + + +class project_manager(base.ResourceManager): + resource_class = Project + + def project_create(self, url, data): + resp = self.http_client.post(url, data) + + # Unauthorized + if resp.status_code == 401: + raise exceptions.Unauthorized('Unauthorized request') + if resp.status_code != 201: + self._raise_api_exception(resp) + + # Converted into python dict + json_object = get_json(resp) + return json_object + + def projects_list(self, url): + resp = self.http_client.get(url) + + # Unauthorized + if resp.status_code == 401: + raise exceptions.Unauthorized('Unauthorized request') + if resp.status_code != 200: + self._raise_api_exception(resp) + + # Converted into python dict + json_objects = get_json(resp) + + projects = [] + for json_object in json_objects: + json_object = json_object['project'] + project = Project( + self, + id=json_object['id'], + domain_id=json_object['domain_id'], + name=json_object['name'], + extra=json_object['extra'], + description=json_object['description'], + enabled=json_object['enabled'], + parent_id=json_object['parent_id'], + is_domain=json_object['is_domain']) + + projects.append(project) + + return projects + + def _project_detail(self, url): + resp = self.http_client.get(url) + + # Unauthorized + if resp.status_code == 401: + raise exceptions.Unauthorized('Unauthorized request') + if resp.status_code != 200: + self._raise_api_exception(resp) + + # Return project details in original json format, + # ie, without convert it into python dict + return resp.content + + def _project_update(self, url, data): + resp = self.http_client.put(url, data) + + # Unauthorized + if resp.status_code == 401: + raise exceptions.Unauthorized('Unauthorized request') + if resp.status_code != 200: + self._raise_api_exception(resp) + + # Converted into python dict + json_object = get_json(resp) + return json_object + + def add_project(self, data): + url = '/identity/projects/' + return self.project_create(url, data) + + def list_projects(self): + url = '/identity/projects/' + return self.projects_list(url) + + def project_detail(self, project_ref): + url = '/identity/projects/%s' % project_ref + return self._project_detail(url) + + def update_project(self, project_ref, data): + url = '/identity/projects/%s' % project_ref + return self._project_update(url, data) diff --git a/dcdbsync/dbsyncclient/v1/identity/role_manager.py b/dcdbsync/dbsyncclient/v1/identity/role_manager.py new file mode 100644 index 000000000..cb982deb9 --- /dev/null +++ b/dcdbsync/dbsyncclient/v1/identity/role_manager.py @@ -0,0 +1,127 @@ +# Copyright (c) 2017 Ericsson AB. +# All Rights Reserved. +# +# 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. +# +# Copyright (c) 2019 Wind River Systems, Inc. +# +# SPDX-License-Identifier: Apache-2.0 +# + +from dcdbsync.dbsyncclient import base +from dcdbsync.dbsyncclient.base import get_json +from dcdbsync.dbsyncclient import exceptions + + +class Role(base.Resource): + resource_name = 'role' + + def __init__(self, manager, id, domain_id, name, extra={}): + self.manager = manager + self.id = id + self.domain_id = domain_id + self.name = name + self.extra = extra + + def info(self): + resource_info = dict() + resource_info.update({self.resource_name: + {'name': self.name, + 'id': self.id, + 'domain_id': self.domain_id}}) + return resource_info + + +class role_manager(base.ResourceManager): + resource_class = Role + + def role_create(self, url, data): + resp = self.http_client.post(url, data) + + # Unauthorized + if resp.status_code == 401: + raise exceptions.Unauthorized('Unauthorized request') + if resp.status_code != 201: + self._raise_api_exception(resp) + + # Converted into python dict + json_object = get_json(resp) + return json_object + + def roles_list(self, url): + resp = self.http_client.get(url) + + # Unauthorized + if resp.status_code == 401: + raise exceptions.Unauthorized('Unauthorized request') + if resp.status_code != 200: + self._raise_api_exception(resp) + + # Converted into python dict + json_objects = get_json(resp) + + roles = [] + for json_object in json_objects: + json_object = json_object.get('role') + role = Role( + self, + id=json_object['id'], + domain_id=json_object['domain_id'], + name=json_object['name'], + extra=json_object['extra']) + + roles.append(role) + + return roles + + def _role_detail(self, url): + resp = self.http_client.get(url) + + # Unauthorized + if resp.status_code == 401: + raise exceptions.Unauthorized('Unauthorized request') + if resp.status_code != 200: + self._raise_api_exception(resp) + + # Return role details in original json format, + # ie, without convert it into python dict + return resp.content + + def _role_update(self, url, data): + resp = self.http_client.put(url, data) + + # Unauthorized + if resp.status_code == 401: + raise exceptions.Unauthorized('Unauthorized request') + if resp.status_code != 200: + self._raise_api_exception(resp) + + # Converted into python dict + json_object = get_json(resp) + return json_object + + def add_role(self, data): + url = '/identity/roles/' + return self.role_create(url, data) + + def list_roles(self): + url = '/identity/roles/' + return self.roles_list(url) + + def role_detail(self, role_ref): + url = '/identity/roles/%s' % role_ref + return self._role_detail(url) + + def update_role(self, role_ref, data): + url = '/identity/roles/%s' % role_ref + return self._role_update(url, data) diff --git a/dcdbsync/dbsyncclient/v1/identity/token_revoke_event_manager.py b/dcdbsync/dbsyncclient/v1/identity/token_revoke_event_manager.py new file mode 100644 index 000000000..f9884e509 --- /dev/null +++ b/dcdbsync/dbsyncclient/v1/identity/token_revoke_event_manager.py @@ -0,0 +1,166 @@ +# Copyright (c) 2017 Ericsson AB. +# All Rights Reserved. +# +# 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. +# +# Copyright (c) 2019 Wind River Systems, Inc. +# +# SPDX-License-Identifier: Apache-2.0 +# + + +from dcdbsync.dbsyncclient import base +from dcdbsync.dbsyncclient.base import get_json +from dcdbsync.dbsyncclient import exceptions + + +class RevokeEvent(base.Resource): + resource_name = 'token_revoke_event' + + def __init__(self, manager, id, domain_id, project_id, user_id, role_id, + trust_id, consumer_id, access_token_id, issued_before, + expires_at, revoked_at, audit_id, audit_chain_id): + self.manager = manager + self.id = id + self.domain_id = domain_id + self.project_id = project_id + self.user_id = user_id + self.role_id = role_id + self.trust_id = trust_id + self.consumer_id = consumer_id + self.access_token_id = access_token_id + self.issued_before = issued_before + self.expires_at = expires_at + self.revoked_at = revoked_at + self.audit_id = audit_id + self.audit_chain_id = audit_chain_id + + def info(self): + resource_info = dict() + resource_info.update({self.resource_name: + {'id': self.id, + 'project_id': self.project_id, + 'user_id': self.user_id, + 'role_id': self.role_id, + 'audit_id': self.audit_id, + 'issued_before': self.issued_before}}) + return resource_info + + +class revoke_event_manager(base.ResourceManager): + resource_class = RevokeEvent + + def revoke_event_create(self, url, data): + resp = self.http_client.post(url, data) + + # Unauthorized + if resp.status_code == 401: + raise exceptions.Unauthorized('Unauthorized request') + if resp.status_code != 201: + self._raise_api_exception(resp) + + # Converted into python dict + json_object = get_json(resp) + return json_object + + def revoke_events_list(self, url): + resp = self.http_client.get(url) + + # Unauthorized + if resp.status_code == 401: + raise exceptions.Unauthorized('Unauthorized request') + if resp.status_code != 200: + self._raise_api_exception(resp) + + # Converted into python dict + json_objects = get_json(resp) + + revoke_events = [] + for json_object in json_objects: + json_object = json_object.get('revocation_event') + revoke_event = RevokeEvent( + self, + id=json_object['id'], + domain_id=json_object['domain_id'], + project_id=json_object['project_id'], + user_id=json_object['user_id'], + role_id=json_object['role_id'], + trust_id=json_object['trust_id'], + consumer_id=json_object['consumer_id'], + access_token_id=json_object['access_token_id'], + issued_before=json_object['issued_before'], + expires_at=json_object['expires_at'], + revoked_at=json_object['revoked_at'], + audit_id=json_object['audit_id'], + audit_chain_id=json_object['audit_chain_id']) + + revoke_events.append(revoke_event) + + return revoke_events + + def _revoke_event_detail(self, url): + resp = self.http_client.get(url) + + # Unauthorized + if resp.status_code == 401: + raise exceptions.Unauthorized('Unauthorized request') + if resp.status_code != 200: + self._raise_api_exception(resp) + + # Return revoke_event details in original json format, + # ie, without convert it into python dict + return resp.content + + def _revoke_event_delete(self, url): + resp = self.http_client.delete(url) + + # Unauthorized + if resp.status_code == 401: + raise exceptions.Unauthorized('Unauthorized request') + # NotFound + if resp.status_code == 404: + raise exceptions.NotFound('Requested item not found') + if resp.status_code != 204: + self._raise_api_exception(resp) + + def add_revoke_event(self, data): + url = '/identity/token-revocation-events/' + return self.revoke_event_create(url, data) + + def list_revoke_events(self): + url = '/identity/token-revocation-events/' + return self.revoke_events_list(url) + + def revoke_event_detail(self, user_id=None, audit_id=None): + if user_id: + url = '/identity/token-revocation-events/users/%s' % user_id + elif audit_id: + url = '/identity/token-revocation-events/audits/%s' % audit_id + else: + raise exceptions.\ + IllegalArgumentException('Token revocation event user ID' + ' or audit ID required.') + + return self._revoke_event_detail(url) + + def delete_revoke_event(self, user_id=None, audit_id=None): + if user_id: + url = '/identity/token-revocation-events/users/%s' % user_id + elif audit_id: + url = '/identity/token-revocation-events/audits/%s' % audit_id + else: + raise exceptions.\ + IllegalArgumentException('Token revocation event ID' + ' or audit ID required.') + + return self._revoke_event_delete(url) diff --git a/dcdbsync/version.py b/dcdbsync/version.py new file mode 100644 index 000000000..2e025d998 --- /dev/null +++ b/dcdbsync/version.py @@ -0,0 +1,20 @@ +# 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. +# +# Copyright (c) 2019 Wind River Systems, Inc. +# +# SPDX-License-Identifier: Apache-2.0 +# + +import pbr.version + +version_info = pbr.version.VersionInfo('distributedcloud') diff --git a/etc/dcdbsync/README-dcdbsync.conf.txt b/etc/dcdbsync/README-dcdbsync.conf.txt new file mode 100644 index 000000000..9a46a949c --- /dev/null +++ b/etc/dcdbsync/README-dcdbsync.conf.txt @@ -0,0 +1,4 @@ +To generate the sample dcdbsync.conf file, run the following +command from the top level of the dcdbsync directory: + +tox -egenconfig diff --git a/etc/dcdbsync/policy.json b/etc/dcdbsync/policy.json new file mode 100755 index 000000000..6c042a136 --- /dev/null +++ b/etc/dcdbsync/policy.json @@ -0,0 +1,5 @@ +{ + "context_is_admin": "role:admin", + "admin_or_owner": "is_admin:True or project_id:%(project_id)s", + "default": "rule:admin_or_owner", +} diff --git a/ocf/dcdbsync-api b/ocf/dcdbsync-api new file mode 100644 index 000000000..2f437587e --- /dev/null +++ b/ocf/dcdbsync-api @@ -0,0 +1,324 @@ +#!/bin/sh +# OpenStack DC Orchestrator DBsync API Service (dcdbsync-api) +# +# Description: Manages a DC Orchestrator DBsync API Service +# (dcdbsync-api) process as an HA resource +# +# Copyright (c) 2019 Wind River Systems, Inc. +# +# SPDX-License-Identifier: Apache-2.0 +# +# +# See usage() function below for more details ... +# +# OCF instance parameters: +# OCF_RESKEY_binary +# OCF_RESKEY_config +# OCF_RESKEY_user +# OCF_RESKEY_pid +# OCF_RESKEY_additional_parameters +####################################################################### +# Initialization: + +: ${OCF_FUNCTIONS_DIR=${OCF_ROOT}/lib/heartbeat} +. ${OCF_FUNCTIONS_DIR}/ocf-shellfuncs + +####################################################################### + +# Fill in some defaults if no values are specified + +OCF_RESKEY_binary_default="dcdbsync-api" +OCF_RESKEY_config_default="/etc/dcdbsync/dcdbsync.conf" +OCF_RESKEY_user_default="root" +OCF_RESKEY_pid_default="$HA_RSCTMP/$OCF_RESOURCE_INSTANCE.pid" + +: ${OCF_RESKEY_binary=${OCF_RESKEY_binary_default}} +: ${OCF_RESKEY_config=${OCF_RESKEY_config_default}} +: ${OCF_RESKEY_user=${OCF_RESKEY_user_default}} +: ${OCF_RESKEY_pid=${OCF_RESKEY_pid_default}} + +####################################################################### + +usage() { + cat < + + +1.0 + + +Resource agent for the DC Orchestrator DBsync API Service (dcdbsync-api) + +Manages the OpenStack DC Orchestrator DBsync API +Service(dcdbsync-api) + + + + +Location of the DC Orchestrator DBsync API server binary (dcdbsync-api) + +DC Orchestrator DBsync API server binary (dcdbsync-api) + + + + + +Location of the DC Orchestrator DBsync API (dcdbsync-api) configuration file + +DC Orchestrator DBsync API (dcdbsync-api registry) config file + + + + + +User running DC Orchestrator DBsync API (dcdbsync-api) + +DC Orchestrator DBsync API (dcdbsync-api) user + + + + + +The pid file to use for this DC Orchestrator DBsync API (dcdbsync-api) instance + +DC Orchestrator DBsync API (dcdbsync-api) pid file + + + + + +Additional parameters to pass on to the OpenStack Orchestrator DBsync API +(dcdbsync-api) + +Additional parameters for dcdbsync-api + + + + + + + + + + + + + + +END +} + +####################################################################### +# Functions invoked by resource manager actions + +dcdbsync_api_validate() { + local rc + + check_binary $OCF_RESKEY_binary + check_binary curl + check_binary tr + check_binary grep + check_binary cut + check_binary head + + # A config file on shared storage that is not available + # during probes is OK. + if [ ! -f $OCF_RESKEY_config ]; then + if ! ocf_is_probe; then + ocf_log err "Config $OCF_RESKEY_config doesn't exist" + return $OCF_ERR_INSTALLED + fi + ocf_log_warn "Config $OCF_RESKEY_config not available during a probe" + fi + + getent passwd $OCF_RESKEY_user >/dev/null 2>&1 + rc=$? + if [ $rc -ne 0 ]; then + ocf_log err "User $OCF_RESKEY_user doesn't exist" + return $OCF_ERR_INSTALLED + fi + + true +} + +dcdbsync_api_status() { + local pid + local rc + + if [ ! -f $OCF_RESKEY_pid ]; then + ocf_log info "DC Orchestrator DBsync API (dcdbsync-api) is not running" + return $OCF_NOT_RUNNING + else + pid=`cat $OCF_RESKEY_pid` + fi + + ocf_run -warn kill -s 0 $pid + rc=$? + if [ $rc -eq 0 ]; then + return $OCF_SUCCESS + else + ocf_log info "Old PID file found, but DC Orchestrator DBsync API (dcdbsync-api) is not running" + rm -f $OCF_RESKEY_pid + return $OCF_NOT_RUNNING + fi +} + +dcdbsync_api_monitor() { + local rc + + dcdbsync_api_status + rc=$? + + # If status returned anything but success, return that immediately + if [ $rc -ne $OCF_SUCCESS ]; then + return $rc + fi + + ocf_log debug "DC Orchestrator DBsync API (dcdbsync-api) monitor succeeded" + return $OCF_SUCCESS +} + +dcdbsync_api_start() { + local rc + + dcdbsync_api_status + rc=$? + if [ $rc -eq $OCF_SUCCESS ]; then + ocf_log info "DC Orchestrator DBsync API (dcdbsync-api) already running" + return $OCF_SUCCESS + fi + + # Change the working dir to /, to be sure it's accesible + cd / + + # run the actual dcdbsync-api daemon. Don't use ocf_run as we're sending the tool's output + # straight to /dev/null anyway and using ocf_run would break stdout-redirection here. + su ${OCF_RESKEY_user} -s /bin/sh -c "${OCF_RESKEY_binary} --config-file=$OCF_RESKEY_config \ + $OCF_RESKEY_additional_parameters"' >> /dev/null 2>&1 & echo $!' > $OCF_RESKEY_pid + + # Spin waiting for the server to come up. + # Let the CRM/LRM time us out if required + while true; do + dcdbsync_api_monitor + rc=$? + [ $rc -eq $OCF_SUCCESS ] && break + if [ $rc -ne $OCF_NOT_RUNNING ]; then + ocf_log err "DC Orchestrator DBsync API (dcdbsync-api) start failed" + exit $OCF_ERR_GENERIC + fi + sleep 1 + done + + ocf_log info "DC Orchestrator DBsync API (dcdbsync-api) started" + return $OCF_SUCCESS +} + +dcdbsync_api_confirm_stop() { + local my_bin + local my_processes + + my_binary=`which ${OCF_RESKEY_binary}` + my_processes=`pgrep -l -f "^(python|/usr/bin/python|/usr/bin/python2) ${my_binary}([^\w-]|$)"` + + if [ -n "${my_processes}" ] + then + ocf_log info "About to SIGKILL the following: ${my_processes}" + pkill -KILL -f "^(python|/usr/bin/python|/usr/bin/python2) ${my_binary}([^\w-]|$)" + fi +} + +dcdbsync_api_stop() { + local rc + local pid + + dcdbsync_api_status + rc=$? + if [ $rc -eq $OCF_NOT_RUNNING ]; then + ocf_log info "DC Orchestrator DBsync API (dcdbsync-api) already stopped" + dcdbsync_api_confirm_stop + return $OCF_SUCCESS + fi + + # Try SIGTERM + pid=`cat $OCF_RESKEY_pid` + ocf_run kill -s TERM $pid + rc=$? + if [ $rc -ne 0 ]; then + ocf_log err "DC Orchestrator DBsync API (dcdbsync-api) couldn't be stopped" + dcdbsync_api_confirm_stop + exit $OCF_ERR_GENERIC + fi + + # stop waiting + shutdown_timeout=15 + if [ -n "$OCF_RESKEY_CRM_meta_timeout" ]; then + shutdown_timeout=$((($OCF_RESKEY_CRM_meta_timeout/1000)-5)) + fi + count=0 + while [ $count -lt $shutdown_timeout ]; do + dcdbsync_api_status + rc=$? + if [ $rc -eq $OCF_NOT_RUNNING ]; then + break + fi + count=`expr $count + 1` + sleep 1 + ocf_log debug "DC Orchestrator DBsync API (dcdbsync-api) still hasn't stopped yet. Waiting ..." + done + + dcdbsync_api_status + rc=$? + if [ $rc -ne $OCF_NOT_RUNNING ]; then + # SIGTERM didn't help either, try SIGKILL + ocf_log info "DC Orchestrator DBsync API (dcdbsync-api) failed to stop after ${shutdown_timeout}s \ + using SIGTERM. Trying SIGKILL ..." + ocf_run kill -s KILL $pid + fi + dcdbsync_api_confirm_stop + + ocf_log info "DC Orchestrator DBsync API (dcdbsync-api) stopped" + + rm -f $OCF_RESKEY_pid + + return $OCF_SUCCESS +} + +####################################################################### + +case "$1" in + meta-data) meta_data + exit $OCF_SUCCESS;; + usage|help) usage + exit $OCF_SUCCESS;; +esac + +# Anything except meta-data and help must pass validation +dcdbsync_api_validate || exit $? + +# What kind of method was invoked? +case "$1" in + start) dcdbsync_api_start;; + stop) dcdbsync_api_stop;; + status) dcdbsync_api_status;; + monitor) dcdbsync_api_monitor;; + validate-all) ;; + *) usage + exit $OCF_ERR_UNIMPLEMENTED;; +esac + diff --git a/setup.cfg b/setup.cfg index aecca1739..82126e9fa 100644 --- a/setup.cfg +++ b/setup.cfg @@ -23,6 +23,7 @@ classifier = packages = dcmanager dcorch + dcdbsync [entry_points] console_scripts = @@ -34,7 +35,7 @@ console_scripts = dcorch-manage = dcorch.cmd.manage:main dcorch-snmp = dcorch.cmd.snmp:main dcorch-api-proxy = dcorch.cmd.api_proxy:main - + dcdbsync-api = dcdbsync.cmd.api:main oslo.config.opts = dcorch.common.config = dcorch.common.config:list_opts @@ -44,6 +45,8 @@ oslo.config.opts = dcorch.engine.dcorch_lock = dcorch.engine.dcorch_lock:list_opts dcmanager.common.config = dcmanager.common.config:list_opts dcmanager.common.api.api_config = dcmanager.api.api_config:list_opts + dcdbsync.common.config = dcdbsync.common.config:list_opts + dcdbsync.common.api.api_config = dcdbsync.api.api_config:list_opts [extract_messages] keywords = _ gettext ngettext l_ lazy_gettext