Keystone DB sync - introduce DB sync service

This update introduces the DB record synchronization service. This new
service provides REST APIs to read/write/update identity database. The
REST APIs is intended to be used internally by DC Orchestrator to
synchronize identity resources between central cloud and subclouds.

This update also contains python client that wraps up the REST APIs
into functions. The client is to be used by DC Orchestrator.

This service supports the synchronization of the following identity
resources:
- users (local users only)
- user passwords
- projects
- roles
- project role assignments
- token revocation events

Story: 2002842
Task: 22787

Change-Id: Idb4aff5bac006fdd904b58c0c8b3d6a0916fbd4b
Signed-off-by: Andy Ning <andy.ning@windriver.com>
(cherry picked from commit 6cdd47b836)
This commit is contained in:
Andy Ning 2019-03-06 14:06:13 -05:00
parent 042d172876
commit d19abe3594
53 changed files with 4299 additions and 1 deletions

23
dcdbsync/__init__.py Normal file
View File

@ -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()

27
dcdbsync/api/README.rst Executable file
View File

@ -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

0
dcdbsync/api/__init__.py Normal file
View File

109
dcdbsync/api/api_config.py Normal file
View File

@ -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

95
dcdbsync/api/app.py Normal file
View File

@ -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()

View File

@ -0,0 +1,11 @@
===============================
controllers
===============================
API request processing
root.py:
API root request
restcomm.py:
common functionality used in API

View File

View File

@ -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)

View File

@ -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)

View File

View File

@ -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'))

View File

@ -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'))

View File

@ -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'))

View File

@ -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)

View File

@ -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
# <user_id>_<issued_before> 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
# <user_id>_<issued_before> 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'))

View File

@ -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)

76
dcdbsync/api/enforcer.py Normal file
View File

@ -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()

9
dcdbsync/cmd/README.rst Executable file
View File

@ -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

0
dcdbsync/cmd/__init__.py Normal file
View File

75
dcdbsync/cmd/api.py Normal file
View File

@ -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()

View File

124
dcdbsync/common/config.py Normal file
View File

@ -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)

146
dcdbsync/common/context.py Normal file
View File

@ -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)

View File

@ -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.")

25
dcdbsync/common/i18n.py Normal file
View File

@ -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

View File

@ -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)

54
dcdbsync/common/policy.py Normal file
View File

@ -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)

View File

@ -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())

View File

@ -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

0
dcdbsync/db/__init__.py Normal file
View File

View File

157
dcdbsync/db/identity/api.py Normal file
View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

View File

@ -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)

View File

@ -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")

View File

@ -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

View File

@ -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

View File

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

20
dcdbsync/version.py Normal file
View File

@ -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')

View File

@ -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

5
etc/dcdbsync/policy.json Executable file
View File

@ -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",
}

324
ocf/dcdbsync-api Normal file
View File

@ -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 <<UEND
usage: $0 (start|stop|validate-all|meta-data|status|monitor)
$0 manages an OpenStack DC Orchestrator DBsync API (dcdbsync-api) process as an HA resource
The 'start' operation starts the dcdbsync-api service.
The 'stop' operation stops the dcdbsync-api service.
The 'validate-all' operation reports whether the parameters are valid
The 'meta-data' operation reports this RA's meta-data information
The 'status' operation reports whether the dcdbsync-api service is running
The 'monitor' operation reports whether the dcdbsync-api service seems to be working
UEND
}
meta_data() {
cat <<END
<?xml version="1.0"?>
<!DOCTYPE resource-agent SYSTEM "ra-api-1.dtd">
<resource-agent name="dcdbsync-api">
<version>1.0</version>
<longdesc lang="en">
Resource agent for the DC Orchestrator DBsync API Service (dcdbsync-api)
</longdesc>
<shortdesc lang="en">Manages the OpenStack DC Orchestrator DBsync API
Service(dcdbsync-api)</shortdesc>
<parameters>
<parameter name="binary" unique="0" required="0">
<longdesc lang="en">
Location of the DC Orchestrator DBsync API server binary (dcdbsync-api)
</longdesc>
<shortdesc lang="en">DC Orchestrator DBsync API server binary (dcdbsync-api)</shortdesc>
<content type="string" default="${OCF_RESKEY_binary_default}" />
</parameter>
<parameter name="config" unique="0" required="0">
<longdesc lang="en">
Location of the DC Orchestrator DBsync API (dcdbsync-api) configuration file
</longdesc>
<shortdesc lang="en">DC Orchestrator DBsync API (dcdbsync-api registry) config file</shortdesc>
<content type="string" default="${OCF_RESKEY_config_default}" />
</parameter>
<parameter name="user" unique="0" required="0">
<longdesc lang="en">
User running DC Orchestrator DBsync API (dcdbsync-api)
</longdesc>
<shortdesc lang="en">DC Orchestrator DBsync API (dcdbsync-api) user</shortdesc>
<content type="string" default="${OCF_RESKEY_user_default}" />
</parameter>
<parameter name="pid" unique="0" required="0">
<longdesc lang="en">
The pid file to use for this DC Orchestrator DBsync API (dcdbsync-api) instance
</longdesc>
<shortdesc lang="en">DC Orchestrator DBsync API (dcdbsync-api) pid file</shortdesc>
<content type="string" default="${OCF_RESKEY_pid_default}" />
</parameter>
<parameter name="additional_parameters" unique="0" required="0">
<longdesc lang="en">
Additional parameters to pass on to the OpenStack Orchestrator DBsync API
(dcdbsync-api)
</longdesc>
<shortdesc lang="en">Additional parameters for dcdbsync-api</shortdesc>
<content type="string" />
</parameter>
</parameters>
<actions>
<action name="start" timeout="20" />
<action name="stop" timeout="20" />
<action name="status" timeout="20" />
<action name="monitor" timeout="10" interval="5" />
<action name="validate-all" timeout="5" />
<action name="meta-data" timeout="5" />
</actions>
</resource-agent>
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

View File

@ -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