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