From d6b03252add9543e042892767ce16874680eb48f Mon Sep 17 00:00:00 2001 From: lvdongbing Date: Tue, 2 Feb 2016 16:29:59 +0800 Subject: [PATCH] Use openstacksdk to communicate with other services Change-Id: I083fc384b79665f9c64efa867a3b6d93f094dd0e --- bilean/api/middleware/context.py | 82 ++++ bilean/api/openstack/__init__.py | 4 +- bilean/api/openstack/v1/util.py | 15 +- bilean/common/config.py | 6 + bilean/common/context.py | 350 +++++------------- bilean/common/policy.py | 124 ++----- .../clients/os => drivers}/__init__.py | 0 bilean/drivers/base.py | 52 +++ bilean/drivers/openstack/__init__.py | 20 + bilean/drivers/openstack/keystone_v3.py | 69 ++++ bilean/drivers/openstack/neutron_v2.py | 125 +++++++ bilean/drivers/openstack/nova_v2.py | 75 ++++ bilean/drivers/openstack/sdk.py | 114 ++++++ bilean/engine/clients/__init__.py | 142 ------- bilean/engine/clients/client_plugin.py | 92 ----- bilean/engine/clients/os/ceilometer.py | 52 --- bilean/engine/clients/os/cinder.py | 99 ----- bilean/engine/clients/os/glance.py | 103 ------ bilean/engine/clients/os/heat.py | 65 ---- bilean/engine/clients/os/keystone.py | 44 --- bilean/engine/clients/os/neutron.py | 119 ------ bilean/engine/clients/os/nova.py | 294 --------------- bilean/engine/clients/os/sahara.py | 51 --- bilean/engine/clients/os/trove.py | 77 ---- bilean/engine/service.py | 54 ++- bilean/engine/user.py | 20 +- bilean/tests/drivers/__init__.py | 0 bilean/tests/drivers/test_driver.py | 47 +++ bilean/tests/drivers/test_sdk.py | 211 +++++++++++ etc/bilean/api-paste.ini | 2 +- requirements.txt | 3 +- setup.cfg | 11 +- tools/setup-service | 19 +- 33 files changed, 996 insertions(+), 1545 deletions(-) create mode 100644 bilean/api/middleware/context.py rename bilean/{engine/clients/os => drivers}/__init__.py (100%) create mode 100644 bilean/drivers/base.py create mode 100644 bilean/drivers/openstack/__init__.py create mode 100644 bilean/drivers/openstack/keystone_v3.py create mode 100644 bilean/drivers/openstack/neutron_v2.py create mode 100644 bilean/drivers/openstack/nova_v2.py create mode 100644 bilean/drivers/openstack/sdk.py delete mode 100644 bilean/engine/clients/__init__.py delete mode 100644 bilean/engine/clients/client_plugin.py delete mode 100644 bilean/engine/clients/os/ceilometer.py delete mode 100644 bilean/engine/clients/os/cinder.py delete mode 100644 bilean/engine/clients/os/glance.py delete mode 100644 bilean/engine/clients/os/heat.py delete mode 100644 bilean/engine/clients/os/keystone.py delete mode 100644 bilean/engine/clients/os/neutron.py delete mode 100644 bilean/engine/clients/os/nova.py delete mode 100644 bilean/engine/clients/os/sahara.py delete mode 100644 bilean/engine/clients/os/trove.py create mode 100644 bilean/tests/drivers/__init__.py create mode 100644 bilean/tests/drivers/test_driver.py create mode 100644 bilean/tests/drivers/test_sdk.py diff --git a/bilean/api/middleware/context.py b/bilean/api/middleware/context.py new file mode 100644 index 0000000..979249d --- /dev/null +++ b/bilean/api/middleware/context.py @@ -0,0 +1,82 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from oslo_config import cfg +from oslo_middleware import request_id as oslo_request_id +from oslo_utils import encodeutils + +from bilean.common import context +from bilean.common import exception +from bilean.common import wsgi + + +class ContextMiddleware(wsgi.Middleware): + + def process_request(self, req): + '''Build context from authentication info extracted from request.''' + + headers = req.headers + environ = req.environ + try: + auth_url = headers.get('X-Auth-Url') + if not auth_url: + # Use auth_url defined in bilean.conf + auth_url = cfg.CONF.authentication.auth_url + + auth_token = headers.get('X-Auth-Token') + auth_token_info = environ.get('keystone.token_info') + + project = headers.get('X-Project-Id') + project_name = headers.get('X-Project-Name') + project_domain = headers.get('X-Project-Domain-Id') + project_domain_name = headers.get('X-Project-Domain-Name') + + user = headers.get('X-User-Id') + user_name = headers.get('X-User-Name') + user_domain = headers.get('X-User-Domain-Id') + user_domain_name = headers.get('X-User-Domain-Name') + + domain = headers.get('X-Domain-Id') + domain_name = headers.get('X-Domain-Name') + + region_name = headers.get('X-Region-Name') + + roles = headers.get('X-Roles') + if roles is not None: + roles = roles.split(',') + + env_req_id = environ.get(oslo_request_id.ENV_REQUEST_ID) + if env_req_id is None: + request_id = None + else: + request_id = encodeutils.safe_decode(env_req_id) + + except Exception: + raise exception.NotAuthenticated() + + req.context = context.RequestContext( + auth_token=auth_token, + user=user, + project=project, + domain=domain, + user_domain=user_domain, + project_domain=project_domain, + request_id=request_id, + auth_url=auth_url, + user_name=user_name, + project_name=project_name, + domain_name=domain_name, + user_domain_name=user_domain_name, + project_domain_name=project_domain_name, + auth_token_info=auth_token_info, + region_name=region_name, + roles=roles) diff --git a/bilean/api/openstack/__init__.py b/bilean/api/openstack/__init__.py index 11c32b0..19de6b4 100644 --- a/bilean/api/openstack/__init__.py +++ b/bilean/api/openstack/__init__.py @@ -11,11 +11,11 @@ # License for the specific language governing permissions and limitations # under the License. +from bilean.api.middleware.context import ContextMiddleware from bilean.api.middleware.fault import FaultWrapper from bilean.api.middleware.ssl import SSLMiddleware from bilean.api.middleware.version_negotiation import VersionNegotiationFilter from bilean.api.openstack import versions -from bilean.common import context def version_negotiation_filter(app, conf, **local_conf): @@ -32,4 +32,4 @@ def sslmiddleware_filter(app, conf, **local_conf): def contextmiddleware_filter(app, conf, **local_conf): - return context.ContextMiddleware(app) + return ContextMiddleware(app) diff --git a/bilean/api/openstack/v1/util.py b/bilean/api/openstack/v1/util.py index 7679997..06305e3 100644 --- a/bilean/api/openstack/v1/util.py +++ b/bilean/api/openstack/v1/util.py @@ -12,10 +12,12 @@ # under the License. import functools -import six +import six from webob import exc +from bilean.common import policy + def policy_enforce(handler): """Decorator that enforces policies. @@ -27,11 +29,14 @@ def policy_enforce(handler): """ @functools.wraps(handler) def handle_bilean_method(controller, req, tenant_id, **kwargs): - if req.context.tenant_id != tenant_id: + import pdb + pdb.set_trace() + if req.context.project != tenant_id: raise exc.HTTPForbidden() - allowed = req.context.policy.enforce(context=req.context, - action=handler.__name__, - scope=controller.REQUEST_SCOPE) + + rule = "%s:%s" % (controller.REQUEST_SCOPE, handler.__name__) + allowed = policy.enforce(context=req.context, + rule=rule, target={}) if not allowed: raise exc.HTTPForbidden() return handler(controller, req, **kwargs) diff --git a/bilean/common/config.py b/bilean/common/config.py index d9c05a5..f18db88 100644 --- a/bilean/common/config.py +++ b/bilean/common/config.py @@ -55,6 +55,11 @@ rpc_opts = [ 'It is not necessarily a hostname, FQDN, ' 'or IP address.'))] +cloud_backend_opts = [ + cfg.StrOpt('cloud_backend', + default='openstack', + help=_('Default cloud backend to use.'))] + authentication_group = cfg.OptGroup('authentication') authentication_opts = [ cfg.StrOpt('auth_url', default='', @@ -104,6 +109,7 @@ revision_opts = [ def list_opts(): yield None, rpc_opts yield None, service_opts + yield None, cloud_backend_opts yield paste_deploy_group.name, paste_deploy_opts yield authentication_group.name, authentication_opts yield revision_group.name, revision_opts diff --git a/bilean/common/context.py b/bilean/common/context.py index 9a20480..dae1a02 100644 --- a/bilean/common/context.py +++ b/bilean/common/context.py @@ -1,91 +1,76 @@ -# -# 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 +# 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. +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. -from keystoneclient import access -from keystoneclient import auth -from keystoneclient.auth.identity import access as access_plugin -from keystoneclient.auth.identity import v3 -from keystoneclient.auth import token_endpoint -from oslo_config import cfg -from oslo_context import context -from oslo_log import log as logging -import oslo_messaging -from oslo_middleware import request_id as oslo_request_id -from oslo_utils import importutils -import six +from oslo_context import context as base_context +from oslo_utils import encodeutils -from bilean.common import exception -from bilean.common.i18n import _LE, _LW from bilean.common import policy -from bilean.common import wsgi from bilean.db import api as db_api -from bilean.engine import clients - -LOG = logging.getLogger(__name__) - -TRUSTEE_CONF_GROUP = 'trustee' -auth.register_conf_options(cfg.CONF, TRUSTEE_CONF_GROUP) -cfg.CONF.import_group('authentication', 'bilean.common.config') +from bilean.drivers import base as driver_base -class RequestContext(context.RequestContext): - """Stores information about the security context. +class RequestContext(base_context.RequestContext): + '''Stores information about the security context. - Under the security context the user accesses the system, as well as - additional request information. - """ + The context encapsulates information related to the user accessing the + the system, as well as additional request information. + ''' - def __init__(self, auth_token=None, username=None, password=None, - aws_creds=None, tenant=None, user_id=None, - tenant_id=None, auth_url=None, roles=None, is_admin=None, - read_only=False, show_deleted=False, - overwrite=True, trust_id=None, trustor_user_id=None, - request_id=None, auth_token_info=None, region_name=None, - auth_plugin=None, trusts_auth_plugin=None, **kwargs): - """Initialisation of the request context. + 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): - :param overwrite: Set to False to ensure that the greenthread local - copy of the index is not overwritten. + '''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, + read_only=read_only, show_deleted=show_deleted, + request_id=request_id) - :param kwargs: Extra arguments that might be present, but we ignore - because they possibly came in from older rpc messages. - """ - super(RequestContext, self).__init__(auth_token=auth_token, - user=username, tenant=tenant, - is_admin=is_admin, - 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) - self.username = username - self.user_id = user_id - self.password = password - self.region_name = region_name - self.aws_creds = aws_creds - self.tenant_id = tenant_id - self.auth_token_info = auth_token_info - self.auth_url = auth_url - self.roles = roles or [] + # we save an additional 'project' internally for use + self.project = project + + # Session for DB access self._session = None - self._clients = None - self.trust_id = trust_id - self.trustor_user_id = trustor_user_id - self.policy = policy.Enforcer() - self._auth_plugin = auth_plugin - self._trusts_auth_plugin = trusts_auth_plugin + 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 = self.policy.check_is_admin(self) + self.is_admin = policy.enforce(self, 'context_is_admin', + target={'project': self.project}, + do_raise=False) else: self.is_admin = is_admin @@ -95,205 +80,48 @@ class RequestContext(context.RequestContext): self._session = db_api.get_session() return self._session - @property - def clients(self): - if self._clients is None: - self._clients = clients.Clients(self) - return self._clients - def to_dict(self): - user_idt = '{user} {tenant}'.format(user=self.user_id or '-', - tenant=self.tenant_id or '-') - - return {'auth_token': self.auth_token, - 'username': self.username, - 'user_id': self.user_id, - 'password': self.password, - 'aws_creds': self.aws_creds, - 'tenant': self.tenant, - 'tenant_id': self.tenant_id, - 'trust_id': self.trust_id, - 'trustor_user_id': self.trustor_user_id, - 'auth_token_info': self.auth_token_info, - 'auth_url': self.auth_url, - 'roles': self.roles, - 'is_admin': self.is_admin, - 'user': self.user, - 'request_id': self.request_id, - 'show_deleted': self.show_deleted, - 'region_name': self.region_name, - 'user_identity': user_idt} + 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) - @property - def keystone_v3_endpoint(self): - if self.auth_url: - return self.auth_url.replace('v2.0', 'v3') - raise exception.AuthorizationFailure() - @property - def trusts_auth_plugin(self): - if self._trusts_auth_plugin: - return self._trusts_auth_plugin +def get_service_context(**kwargs): + '''An abstraction layer for getting service credential. - self._trusts_auth_plugin = auth.load_from_conf_options( - cfg.CONF, TRUSTEE_CONF_GROUP, trust_id=self.trust_id) - - if self._trusts_auth_plugin: - return self._trusts_auth_plugin - - LOG.warn(_LW('Using the keystone_authtoken user as the bilean ' - 'trustee user directly is deprecated. Please add the ' - 'trustee credentials you need to the %s section of ' - 'your bilean.conf file.') % TRUSTEE_CONF_GROUP) - - cfg.CONF.import_group('keystone_authtoken', - 'keystonemiddleware.auth_token') - - self._trusts_auth_plugin = v3.Password( - username=cfg.CONF.keystone_authtoken.admin_user, - password=cfg.CONF.keystone_authtoken.admin_password, - user_domain_id='default', - auth_url=self.keystone_v3_endpoint, - trust_id=self.trust_id) - return self._trusts_auth_plugin - - def _create_auth_plugin(self): - if self.auth_token_info: - auth_ref = access.AccessInfo.factory(body=self.auth_token_info, - auth_token=self.auth_token) - return access_plugin.AccessInfoPlugin( - auth_url=self.keystone_v3_endpoint, - auth_ref=auth_ref) - - if self.auth_token: - # FIXME(jamielennox): This is broken but consistent. If you - # only have a token but don't load a service catalog then - # url_for wont work. Stub with the keystone endpoint so at - # least it might be right. - return token_endpoint.Token(endpoint=self.keystone_v3_endpoint, - token=self.auth_token) - - if self.password: - return v3.Password(username=self.username, - password=self.password, - project_id=self.tenant_id, - user_domain_id='default', - auth_url=self.keystone_v3_endpoint) - - LOG.error(_LE("Keystone v3 API connection failed, no password " - "trust or auth_token!")) - raise exception.AuthorizationFailure() - - def reload_auth_plugin(self): - self._auth_plugin = None - - @property - def auth_plugin(self): - if not self._auth_plugin: - if self.trust_id: - self._auth_plugin = self.trusts_auth_plugin - else: - self._auth_plugin = self._create_auth_plugin() - - return self._auth_plugin + There could be multiple cloud backends for bilean to use. This + abstraction layer provides an indirection for bilean to get the + credentials of 'bilean' user on the specific cloud. By default, + this credential refers to the credentials built for keystone middleware + in an OpenStack cloud. + ''' + identity_service = driver_base.BileanDriver().identity + service_creds = identity_service.get_service_credentials(**kwargs) + return RequestContext(**service_creds) def get_admin_context(show_deleted=False): return RequestContext(is_admin=True, show_deleted=show_deleted) - - -def get_service_context(show_deleted=False): - conf = cfg.CONF.authentication - return RequestContext(username=conf.service_username, - password=conf.service_password, - tenant=conf.service_project_name, - auth_url=conf.auth_url) - - -class ContextMiddleware(wsgi.Middleware): - - def __init__(self, app, conf, **local_conf): - # Determine the context class to use - self.ctxcls = RequestContext - if 'context_class' in local_conf: - self.ctxcls = importutils.import_class(local_conf['context_class']) - - super(ContextMiddleware, self).__init__(app) - - def make_context(self, *args, **kwargs): - """Create a context with the given arguments.""" - return self.ctxcls(*args, **kwargs) - - def process_request(self, req): - """Constructs an appropriate context from extracted auth information. - - Extract any authentication information in the request and construct an - appropriate context from it. - """ - headers = req.headers - environ = req.environ - - try: - username = None - password = None - aws_creds = None - - if headers.get('X-Auth-User') is not None: - username = headers.get('X-Auth-User') - password = headers.get('X-Auth-Key') - elif headers.get('X-Auth-EC2-Creds') is not None: - aws_creds = headers.get('X-Auth-EC2-Creds') - - user_id = headers.get('X-User-Id') - token = headers.get('X-Auth-Token') - tenant = headers.get('X-Project-Name') - tenant_id = headers.get('X-Project-Id') - region_name = headers.get('X-Region-Name') - auth_url = headers.get('X-Auth-Url') - roles = headers.get('X-Roles') - if roles is not None: - roles = roles.split(',') - token_info = environ.get('keystone.token_info') - auth_plugin = environ.get('keystone.token_auth') - req_id = environ.get(oslo_request_id.ENV_REQUEST_ID) - - except Exception: - raise exception.NotAuthenticated() - - req.context = self.make_context(auth_token=token, - tenant=tenant, tenant_id=tenant_id, - aws_creds=aws_creds, - username=username, - user_id=user_id, - password=password, - auth_url=auth_url, - roles=roles, - request_id=req_id, - auth_token_info=token_info, - region_name=region_name, - auth_plugin=auth_plugin) - - -def ContextMiddleware_filter_factory(global_conf, **local_conf): - """Factory method for paste.deploy.""" - conf = global_conf.copy() - conf.update(local_conf) - - def filter(app): - return ContextMiddleware(app, conf) - - return filter - - -def request_context(func): - @six.wraps(func) - def wrapped(self, ctx, *args, **kwargs): - try: - return func(self, ctx, *args, **kwargs) - except exception.BileanException: - raise oslo_messaging.rpc.dispatcher.ExpectedException() - return wrapped diff --git a/bilean/common/policy.py b/bilean/common/policy.py index 323f0ac..8696e45 100644 --- a/bilean/common/policy.py +++ b/bilean/common/policy.py @@ -1,115 +1,47 @@ -# -# Copyright (c) 2011 OpenStack Foundation -# 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 +# 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. +# 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. -# Based on glance/api/policy.py -"""Policy Engine For Bilean.""" +""" +Policy Engine For Bilean +""" from oslo_config import cfg -from oslo_log import log as logging from oslo_policy import policy -import six from bilean.common import exception - +POLICY_ENFORCER = None CONF = cfg.CONF -LOG = logging.getLogger(__name__) - -DEFAULT_RULES = policy.Rules.from_dict({'default': '!'}) -DEFAULT_RESOURCE_RULES = policy.Rules.from_dict({'default': '@'}) -class Enforcer(object): - """Responsible for loading and enforcing rules.""" +def _get_enforcer(policy_file=None, rules=None, default_rule=None): - def __init__(self, scope='bilean', exc=exception.Forbidden, - default_rule=DEFAULT_RULES['default'], policy_file=None): - self.scope = scope - self.exc = exc - self.default_rule = default_rule - self.enforcer = policy.Enforcer( - CONF, default_rule=default_rule, policy_file=policy_file) + global POLICY_ENFORCER - def set_rules(self, rules, overwrite=True): - """Create a new Rules object based on the provided dict of rules.""" - rules_obj = policy.Rules(rules, self.default_rule) - self.enforcer.set_rules(rules_obj, overwrite) - - def load_rules(self, force_reload=False): - """Set the rules found in the json file on disk.""" - self.enforcer.load_rules(force_reload) - - def _check(self, context, rule, target, exc, *args, **kwargs): - """Verifies that the action is valid on the target in this context. - - :param context: Bilean request context - :param rule: String representing the action to be checked - :param target: Dictionary representing the object of the action. - :raises: self.exc (defaults to bilean.common.exception.Forbidden) - :returns: A non-False value if access is allowed. - """ - do_raise = False if not exc else True - credentials = context.to_dict() - return self.enforcer.enforce(rule, target, credentials, - do_raise, exc=exc, *args, **kwargs) - - def enforce(self, context, action, scope=None, target=None): - """Verifies that the action is valid on the target in this context. - - :param context: Bilean request context - :param action: String representing the action to be checked - :param target: Dictionary representing the object of the action. - :raises: self.exc (defaults to bilean.common.exception.Forbidden) - :returns: A non-False value if access is allowed. - """ - _action = '%s:%s' % (scope or self.scope, action) - _target = target or {} - return self._check(context, _action, _target, self.exc, action=action) - - def check_is_admin(self, context): - """Whether or not roles contains 'admin' role according to policy.json. - - :param context: Bilean request context - :returns: A non-False value if the user is admin according to policy - """ - return self._check(context, 'context_is_admin', target={}, exc=None) + if POLICY_ENFORCER is None: + POLICY_ENFORCER = policy.Enforcer(CONF, + policy_file=policy_file, + rules=rules, + default_rule=default_rule) + return POLICY_ENFORCER -class ResourceEnforcer(Enforcer): - def __init__(self, default_rule=DEFAULT_RESOURCE_RULES['default'], - **kwargs): - super(ResourceEnforcer, self).__init__( - default_rule=default_rule, **kwargs) +def enforce(context, rule, target, do_raise=True, *args, **kwargs): - def enforce(self, context, res_type, scope=None, target=None): - # NOTE(pas-ha): try/except just to log the exception - try: - result = super(ResourceEnforcer, self).enforce( - context, res_type, - scope=scope or 'resource_types', - target=target) - except self.exc as ex: - LOG.info(six.text_type(ex)) - raise - if not result: - if self.exc: - raise self.exc(action=res_type) - else: - return result + enforcer = _get_enforcer() + credentials = context.to_dict() + target = target or {} + if do_raise: + kwargs.update(exc=exception.Forbidden) - def enforce_stack(self, stack, scope=None, target=None): - for res in stack.resources.values(): - self.enforce(stack.context, res.type(), scope=scope, target=target) + return enforcer.enforce(rule, target, credentials, do_raise, + *args, **kwargs) diff --git a/bilean/engine/clients/os/__init__.py b/bilean/drivers/__init__.py similarity index 100% rename from bilean/engine/clients/os/__init__.py rename to bilean/drivers/__init__.py diff --git a/bilean/drivers/base.py b/bilean/drivers/base.py new file mode 100644 index 0000000..4e0f35d --- /dev/null +++ b/bilean/drivers/base.py @@ -0,0 +1,52 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import copy + +from oslo_config import cfg + +from bilean.engine import environment + +CONF = cfg.CONF + + +class DriverBase(object): + '''Base class for all drivers.''' + + def __init__(self, params=None): + if params is None: + params = { + 'auth_url': CONF.authentication.auth_url, + 'username': CONF.authentication.service_username, + 'password': CONF.authentication.service_password, + 'project_name': CONF.authentication.service_project_name, + 'user_domain_name': + cfg.CONF.authentication.service_user_domain, + 'project_domain_name': + cfg.CONF.authentication.service_project_domain, + } + self.conn_params = copy.deepcopy(params) + + +class BileanDriver(object): + '''Generic driver class''' + + def __init__(self, backend_name=None): + + if backend_name is None: + backend_name = cfg.CONF.cloud_backend + + backend = environment.global_env().get_driver(backend_name) + + self.compute = backend.compute + self.network = backend.network + self.identity = backend.identity diff --git a/bilean/drivers/openstack/__init__.py b/bilean/drivers/openstack/__init__.py new file mode 100644 index 0000000..2123d30 --- /dev/null +++ b/bilean/drivers/openstack/__init__.py @@ -0,0 +1,20 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from bilean.drivers.openstack import keystone_v3 +from bilean.drivers.openstack import neutron_v2 +from bilean.drivers.openstack import nova_v2 + + +compute = nova_v2.NovaClient +identity = keystone_v3.KeystoneClient +network = neutron_v2.NeutronClient diff --git a/bilean/drivers/openstack/keystone_v3.py b/bilean/drivers/openstack/keystone_v3.py new file mode 100644 index 0000000..135c6eb --- /dev/null +++ b/bilean/drivers/openstack/keystone_v3.py @@ -0,0 +1,69 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from oslo_config import cfg +from oslo_log import log + +from bilean.drivers import base +from bilean.drivers.openstack import sdk + +LOG = log.getLogger(__name__) +CONF = cfg.CONF + + +class KeystoneClient(base.DriverBase): + '''Keystone V3 driver.''' + + def __init__(self, params=None): + super(KeystoneClient, self).__init__(params) + self.conn = sdk.create_connection(self.conn_params) + + @sdk.translate_exception + def project_find(self, name_or_id, ignore_missing=True): + '''Find a single project + + :param name_or_id: The name or ID of a project. + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the resource does not exist. + When set to ``True``, None will be returned when + attempting to find a nonexistent resource. + :returns: One :class:`~openstack.identity.v3.project.Project` or None + ''' + project = self.conn.identity.find_project( + name_or_id, ignore_missing=ignore_missing) + return project + + @sdk.translate_exception + def project_list(self, **queries): + '''Function to get project list.''' + return self.conn.identity.projects(**queries) + + @classmethod + def get_service_credentials(cls, **kwargs): + '''Bilean service credential to use with Keystone. + + :param kwargs: An additional keyword argument list that can be used + for customizing the default settings. + ''' + + creds = { + 'auth_url': CONF.authentication.auth_url, + 'username': CONF.authentication.service_username, + 'password': CONF.authentication.service_password, + 'project_name': CONF.authentication.service_project_name, + 'user_domain_name': cfg.CONF.authentication.service_user_domain, + 'project_domain_name': + cfg.CONF.authentication.service_project_domain, + } + creds.update(**kwargs) + return creds diff --git a/bilean/drivers/openstack/neutron_v2.py b/bilean/drivers/openstack/neutron_v2.py new file mode 100644 index 0000000..ce9424f --- /dev/null +++ b/bilean/drivers/openstack/neutron_v2.py @@ -0,0 +1,125 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from bilean.drivers import base +from bilean.drivers.openstack import sdk + + +class NeutronClient(base.DriverBase): + '''Neutron V2 driver.''' + + def __init__(self, params=None): + super(NeutronClient, self).__init__(params) + self.conn = sdk.create_connection(self.conn_params) + + @sdk.translate_exception + def network_get(self, name_or_id): + network = self.conn.network.find_network(name_or_id) + return network + + @sdk.translate_exception + def network_delete(self, network, ignore_missing=True): + self.conn.network.delete_network( + network, ignore_missing=ignore_missing) + return + + @sdk.translate_exception + def subnet_get(self, name_or_id): + subnet = self.conn.network.find_subnet(name_or_id) + return subnet + + @sdk.translate_exception + def subnet_delete(self, subnet, ignore_missing=True): + self.conn.network.delete_subnet( + subnet, ignore_missing=ignore_missing) + return + + @sdk.translate_exception + def loadbalancer_get(self, name_or_id): + lb = self.conn.network.find_load_balancer(name_or_id) + return lb + + @sdk.translate_exception + def loadbalancer_list(self): + lbs = [lb for lb in self.conn.network.load_balancers()] + return lbs + + @sdk.translate_exception + def loadbalancer_delete(self, lb_id, ignore_missing=True): + self.conn.network.delete_load_balancer( + lb_id, ignore_missing=ignore_missing) + return + + @sdk.translate_exception + def listener_get(self, name_or_id): + listener = self.conn.network.find_listener(name_or_id) + return listener + + @sdk.translate_exception + def listener_list(self): + listeners = [i for i in self.conn.network.listeners()] + return listeners + + @sdk.translate_exception + def listener_delete(self, listener_id, ignore_missing=True): + self.conn.network.delete_listener(listener_id, + ignore_missing=ignore_missing) + return + + @sdk.translate_exception + def pool_get(self, name_or_id): + pool = self.conn.network.find_pool(name_or_id) + return pool + + @sdk.translate_exception + def pool_list(self): + pools = [p for p in self.conn.network.pools()] + return pools + + @sdk.translate_exception + def pool_delete(self, pool_id, ignore_missing=True): + self.conn.network.delete_pool(pool_id, + ignore_missing=ignore_missing) + return + + @sdk.translate_exception + def pool_member_get(self, pool_id, name_or_id): + member = self.conn.network.find_pool_member(name_or_id, + pool_id) + return member + + @sdk.translate_exception + def pool_member_list(self, pool_id): + members = [m for m in self.conn.network.pool_members(pool_id)] + return members + + @sdk.translate_exception + def pool_member_delete(self, pool_id, member_id, ignore_missing=True): + self.conn.network.delete_pool_member( + member_id, pool_id, ignore_missing=ignore_missing) + return + + @sdk.translate_exception + def healthmonitor_get(self, name_or_id): + hm = self.conn.network.find_health_monitor(name_or_id) + return hm + + @sdk.translate_exception + def healthmonitor_list(self): + hms = [hm for hm in self.conn.network.health_monitors()] + return hms + + @sdk.translate_exception + def healthmonitor_delete(self, hm_id, ignore_missing=True): + self.conn.network.delete_health_monitor( + hm_id, ignore_missing=ignore_missing) + return diff --git a/bilean/drivers/openstack/nova_v2.py b/bilean/drivers/openstack/nova_v2.py new file mode 100644 index 0000000..89d4388 --- /dev/null +++ b/bilean/drivers/openstack/nova_v2.py @@ -0,0 +1,75 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from oslo_config import cfg +from oslo_log import log + +from bilean.drivers import base +from bilean.drivers.openstack import sdk + +LOG = log.getLogger(__name__) + + +class NovaClient(base.DriverBase): + '''Nova V2 driver.''' + + def __init__(self, params=None): + super(NovaClient, self).__init__(params) + self.conn = sdk.create_connection(self.conn_params) + + @sdk.translate_exception + def flavor_find(self, name_or_id, ignore_missing=False): + return self.conn.compute.find_flavor(name_or_id, ignore_missing) + + @sdk.translate_exception + def flavor_list(self, details=True, **query): + return self.conn.compute.flavors(details, **query) + + @sdk.translate_exception + def image_find(self, name_or_id, ignore_missing=False): + return self.conn.compute.find_image(name_or_id, ignore_missing) + + @sdk.translate_exception + def image_list(self, details=True, **query): + return self.conn.compute.images(details, **query) + + @sdk.translate_exception + def image_delete(self, value, ignore_missing=True): + return self.conn.compute.delete_image(value, ignore_missing) + + @sdk.translate_exception + def server_get(self, value): + return self.conn.compute.get_server(value) + + @sdk.translate_exception + def server_list(self, details=True, **query): + return self.conn.compute.servers(details, **query) + + @sdk.translate_exception + def server_update(self, value, **attrs): + return self.conn.compute.update_server(value, **attrs) + + @sdk.translate_exception + def server_delete(self, value, ignore_missing=True): + return self.conn.compute.delete_server(value, ignore_missing) + + @sdk.translate_exception + def wait_for_server_delete(self, value, timeout=None): + '''Wait for server deleting complete''' + if timeout is None: + timeout = cfg.CONF.default_action_timeout + + server_obj = self.conn.compute.find_server(value, True) + if server_obj: + self.conn.compute.wait_for_delete(server_obj, wait=timeout) + + return diff --git a/bilean/drivers/openstack/sdk.py b/bilean/drivers/openstack/sdk.py new file mode 100644 index 0000000..cd50eec --- /dev/null +++ b/bilean/drivers/openstack/sdk.py @@ -0,0 +1,114 @@ +# 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. + +''' +SDK Client +''' +import functools +from oslo_log import log as logging +import six + +from openstack import connection +from openstack import exceptions as sdk_exc +from openstack import profile +from oslo_serialization import jsonutils +from requests import exceptions as req_exc + +from bilean.common import exception as bilean_exc + +USER_AGENT = 'bilean' +exc = sdk_exc +LOG = logging.getLogger(__name__) + + +def parse_exception(ex): + '''Parse exception code and yield useful information.''' + code = 500 + + if isinstance(ex, sdk_exc.HttpException): + # some exceptions don't contain status_code + if ex.http_status is not None: + code = ex.http_status + message = ex.message + data = {} + try: + data = jsonutils.loads(ex.details) + except Exception: + # Some exceptions don't have details record or + # are not in JSON format + pass + + # try dig more into the exception record + # usually 'data' has two types of format : + # type1: {"forbidden": {"message": "error message", "code": 403} + # type2: {"code": 404, "error": { "message": "not found"}} + if data: + code = data.get('code', code) + message = data.get('message', message) + error = data.get('error', None) + if error: + code = data.get('code', code) + message = data['error'].get('message', message) + else: + for value in data.values(): + code = value.get('code', code) + message = value.get('message', message) + + elif isinstance(ex, sdk_exc.SDKException): + # Besides HttpException there are some other exceptions like + # ResourceTimeout can be raised from SDK, handle them here. + message = ex.message + elif isinstance(ex, req_exc.RequestException): + # Exceptions that are not captured by SDK + code = ex.errno + message = six.text_type(ex) + elif isinstance(ex, Exception): + message = six.text_type(ex) + + raise bilean_exc.InternalError(code=code, message=message) + + +def translate_exception(func): + """Decorator for exception translation.""" + + @functools.wraps(func) + def invoke_with_catch(driver, *args, **kwargs): + try: + return func(driver, *args, **kwargs) + except Exception as ex: + LOG.exception(ex) + raise parse_exception(ex) + + return invoke_with_catch + + +def create_connection(params=None): + if params is None: + params = {} + + if params.get('token', None): + auth_plugin = 'token' + else: + auth_plugin = 'password' + + prof = profile.Profile() + prof.set_version('identity', 'v3') + if 'region_name' in params: + prof.set_region(prof.ALL, params['region_name']) + params.pop('region_name') + try: + conn = connection.Connection(profile=prof, user_agent=USER_AGENT, + auth_plugin=auth_plugin, **params) + except Exception as ex: + raise parse_exception(ex) + + return conn diff --git a/bilean/engine/clients/__init__.py b/bilean/engine/clients/__init__.py deleted file mode 100644 index 56468de..0000000 --- a/bilean/engine/clients/__init__.py +++ /dev/null @@ -1,142 +0,0 @@ -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -import weakref - -from oslo_config import cfg -from oslo_log import log as logging -from oslo_utils import importutils -import six -from stevedore import enabled - -from bilean.common import exception -from bilean.common.i18n import _ -from bilean.common.i18n import _LW - -LOG = logging.getLogger(__name__) - - -_default_backend = "bilean.engine.clients.OpenStackClients" - -cloud_opts = [ - cfg.StrOpt('client_backend', - default=_default_backend, - help="Fully qualified class name to use as a client backend.") -] -cfg.CONF.register_opts(cloud_opts) - - -class OpenStackClients(object): - """Convenience class to create and cache client instances.""" - - def __init__(self, context): - self._context = weakref.ref(context) - self._clients = {} - self._client_plugins = {} - - @property - def context(self): - ctxt = self._context() - assert ctxt is not None, "Need a reference to the context" - return ctxt - - def invalidate_plugins(self): - """Used to force plugins to clear any cached client.""" - for name in self._client_plugins: - self._client_plugins[name].invalidate() - - def client_plugin(self, name): - global _mgr - if name in self._client_plugins: - return self._client_plugins[name] - if _mgr and name in _mgr.names(): - client_plugin = _mgr[name].plugin(self.context) - self._client_plugins[name] = client_plugin - return client_plugin - - def client(self, name): - client_plugin = self.client_plugin(name) - if client_plugin: - return client_plugin.client() - - if name in self._clients: - return self._clients[name] - # call the local method _() if a real client plugin - # doesn't exist - method_name = '_%s' % name - if callable(getattr(self, method_name, None)): - client = getattr(self, method_name)() - self._clients[name] = client - return client - LOG.warn(_LW('Requested client "%s" not found'), name) - - @property - def auth_token(self): - # Always use the auth_token from the keystone() client, as - # this may be refreshed if the context contains credentials - # which allow reissuing of a new token before the context - # auth_token expiry (e.g trust_id or username/password) - return self.client('keystone').auth_token - - -class ClientBackend(object): - """Class for delaying choosing the backend client module. - - Delay choosing the backend client module until the client's class needs - to be initialized. - """ - def __new__(cls, context): - if cfg.CONF.client_backend == _default_backend: - return OpenStackClients(context) - else: - try: - return importutils.import_object(cfg.CONF.cloud_backend, - context) - except (ImportError, RuntimeError, cfg.NoSuchOptError) as err: - msg = _('Invalid cloud_backend setting in bilean.conf ' - 'detected - %s') % six.text_type(err) - LOG.error(msg) - raise exception.Invalid(reason=msg) - - -Clients = ClientBackend - - -_mgr = None - - -def has_client(name): - return _mgr and name in _mgr.names() - - -def initialise(): - global _mgr - if _mgr: - return - - def client_is_available(client_plugin): - if not hasattr(client_plugin.plugin, 'is_available'): - # if the client does not have a is_available() class method, then - # we assume it wants to be always available - return True - # let the client plugin decide if it wants to register or not - return client_plugin.plugin.is_available() - - _mgr = enabled.EnabledExtensionManager( - namespace='bilean.clients', - check_func=client_is_available, - invoke_on_load=False) - - -def list_opts(): - yield None, cloud_opts diff --git a/bilean/engine/clients/client_plugin.py b/bilean/engine/clients/client_plugin.py deleted file mode 100644 index 171c007..0000000 --- a/bilean/engine/clients/client_plugin.py +++ /dev/null @@ -1,92 +0,0 @@ -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -import abc -import six - -from oslo_config import cfg - - -@six.add_metaclass(abc.ABCMeta) -class ClientPlugin(object): - - # Module which contains all exceptions classes which the client - # may emit - exceptions_module = None - - def __init__(self, context): - self.context = context - self.clients = context.clients - self._client = None - - def client(self): - if not self._client: - self._client = self._create() - return self._client - - @abc.abstractmethod - def _create(self): - '''Return a newly created client.''' - pass - - @property - def auth_token(self): - # Always use the auth_token from the keystone client, as - # this may be refreshed if the context contains credentials - # which allow reissuing of a new token before the context - # auth_token expiry (e.g trust_id or username/password) - return self.clients.client('keystone').auth_token - - def url_for(self, **kwargs): - kc = self.clients.client('keystone') - return kc.service_catalog.url_for(**kwargs) - - def _get_client_option(self, client, option): - # look for the option in the [clients_${client}] section - # unknown options raise cfg.NoSuchOptError - try: - group_name = 'clients_' + client - cfg.CONF.import_opt(option, 'bilean.common.config', - group=group_name) - v = getattr(getattr(cfg.CONF, group_name), option) - if v is not None: - return v - except cfg.NoSuchGroupError: - pass # do not error if the client is unknown - # look for the option in the generic [clients] section - cfg.CONF.import_opt(option, 'bilean.common.config', group='clients') - return getattr(cfg.CONF.clients, option) - - def is_client_exception(self, ex): - '''Returns True if the current exception comes from the client.''' - if self.exceptions_module: - if isinstance(self.exceptions_module, list): - for m in self.exceptions_module: - if type(ex) in m.__dict__.values(): - return True - else: - return type(ex) in self.exceptions_module.__dict__.values() - return False - - def is_not_found(self, ex): - '''Returns True if the exception is a not-found.''' - return False - - def is_over_limit(self, ex): - '''Returns True if the exception is an over-limit.''' - return False - - def ignore_not_found(self, ex): - '''Raises the exception unless it is a not-found.''' - if not self.is_not_found(ex): - raise ex diff --git a/bilean/engine/clients/os/ceilometer.py b/bilean/engine/clients/os/ceilometer.py deleted file mode 100644 index b41e287..0000000 --- a/bilean/engine/clients/os/ceilometer.py +++ /dev/null @@ -1,52 +0,0 @@ -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -from ceilometerclient import client as cc -from ceilometerclient import exc -from ceilometerclient.openstack.common.apiclient import exceptions as api_exc - -from bilean.engine.clients import client_plugin - - -class CeilometerClientPlugin(client_plugin.ClientPlugin): - - exceptions_module = [exc, api_exc] - - def _create(self): - - con = self.context - endpoint_type = self._get_client_option('ceilometer', 'endpoint_type') - endpoint = self.url_for(service_type='metering', - endpoint_type=endpoint_type) - args = { - 'auth_url': con.auth_url, - 'service_type': 'metering', - 'project_id': con.tenant, - 'token': lambda: self.auth_token, - 'endpoint_type': endpoint_type, - 'cacert': self._get_client_option('ceilometer', 'ca_file'), - 'cert_file': self._get_client_option('ceilometer', 'cert_file'), - 'key_file': self._get_client_option('ceilometer', 'key_file'), - 'insecure': self._get_client_option('ceilometer', 'insecure') - } - - return cc.Client('2', endpoint, **args) - - def is_not_found(self, ex): - return isinstance(ex, (exc.HTTPNotFound, api_exc.NotFound)) - - def is_over_limit(self, ex): - return isinstance(ex, exc.HTTPOverLimit) - - def is_conflict(self, ex): - return isinstance(ex, exc.HTTPConflict) diff --git a/bilean/engine/clients/os/cinder.py b/bilean/engine/clients/os/cinder.py deleted file mode 100644 index 4ffe17b..0000000 --- a/bilean/engine/clients/os/cinder.py +++ /dev/null @@ -1,99 +0,0 @@ -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -from bilean.common import exception -from bilean.common.i18n import _ -from bilean.common.i18n import _LI -from bilean.engine.clients import client_plugin - -from cinderclient import client as cc -from cinderclient import exceptions -from keystoneclient import exceptions as ks_exceptions - -from oslo_log import log as logging - -LOG = logging.getLogger(__name__) - - -class CinderClientPlugin(client_plugin.ClientPlugin): - - exceptions_module = exceptions - - def get_volume_api_version(self): - '''Returns the most recent API version.''' - - endpoint_type = self._get_client_option('cinder', 'endpoint_type') - try: - self.url_for(service_type='volumev2', endpoint_type=endpoint_type) - return 2 - except ks_exceptions.EndpointNotFound: - try: - self.url_for(service_type='volume', - endpoint_type=endpoint_type) - return 1 - except ks_exceptions.EndpointNotFound: - return None - - def _create(self): - - con = self.context - - volume_api_version = self.get_volume_api_version() - if volume_api_version == 1: - service_type = 'volume' - client_version = '1' - elif volume_api_version == 2: - service_type = 'volumev2' - client_version = '2' - else: - raise exception.Error(_('No volume service available.')) - LOG.info(_LI('Creating Cinder client with volume API version %d.'), - volume_api_version) - - endpoint_type = self._get_client_option('cinder', 'endpoint_type') - args = { - 'service_type': service_type, - 'auth_url': con.auth_url or '', - 'project_id': con.tenant, - 'username': None, - 'api_key': None, - 'endpoint_type': endpoint_type, - 'http_log_debug': self._get_client_option('cinder', - 'http_log_debug'), - 'cacert': self._get_client_option('cinder', 'ca_file'), - 'insecure': self._get_client_option('cinder', 'insecure') - } - - client = cc.Client(client_version, **args) - management_url = self.url_for(service_type=service_type, - endpoint_type=endpoint_type) - client.client.auth_token = self.auth_token - client.client.management_url = management_url - - client.volume_api_version = volume_api_version - - return client - - def is_not_found(self, ex): - return isinstance(ex, exceptions.NotFound) - - def is_over_limit(self, ex): - return isinstance(ex, exceptions.OverLimit) - - def is_conflict(self, ex): - return (isinstance(ex, exceptions.ClientException) and - ex.code == 409) - - def delete(self, volume_id): - """Delete a volume by given volume id""" - self.client().volumes.delete(volume_id) diff --git a/bilean/engine/clients/os/glance.py b/bilean/engine/clients/os/glance.py deleted file mode 100644 index e595e84..0000000 --- a/bilean/engine/clients/os/glance.py +++ /dev/null @@ -1,103 +0,0 @@ -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -from bilean.common import exception -from bilean.common.i18n import _ -from bilean.common.i18n import _LI -from bilean.engine.clients import client_plugin - -from oslo_log import log as logging -from oslo_utils import uuidutils - -from glanceclient import client as gc -from glanceclient import exc - -LOG = logging.getLogger(__name__) - - -class GlanceClientPlugin(client_plugin.ClientPlugin): - - exceptions_module = exc - - def _create(self): - - con = self.context - endpoint_type = self._get_client_option('glance', 'endpoint_type') - endpoint = self.url_for(service_type='image', - endpoint_type=endpoint_type) - args = { - 'auth_url': con.auth_url, - 'service_type': 'image', - 'project_id': con.tenant, - 'token': self.auth_token, - 'endpoint_type': endpoint_type, - 'cacert': self._get_client_option('glance', 'ca_file'), - 'cert_file': self._get_client_option('glance', 'cert_file'), - 'key_file': self._get_client_option('glance', 'key_file'), - 'insecure': self._get_client_option('glance', 'insecure') - } - - return gc.Client('1', endpoint, **args) - - def is_not_found(self, ex): - return isinstance(ex, exc.HTTPNotFound) - - def is_over_limit(self, ex): - return isinstance(ex, exc.HTTPOverLimit) - - def is_conflict(self, ex): - return isinstance(ex, exc.HTTPConflict) - - def get_image_id(self, image_identifier): - '''Return an id for the specified image name or identifier. - - :param image_identifier: image name or a UUID-like identifier - :returns: the id of the requested :image_identifier: - :raises: exception.ImageNotFound, - exception.PhysicalResourceNameAmbiguity - ''' - if uuidutils.is_uuid_like(image_identifier): - try: - image_id = self.client().images.get(image_identifier).id - except exc.HTTPNotFound: - image_id = self.get_image_id_by_name(image_identifier) - else: - image_id = self.get_image_id_by_name(image_identifier) - return image_id - - def get_image_id_by_name(self, image_identifier): - '''Return an id for the specified image name. - - :param image_identifier: image name - :returns: the id of the requested :image_identifier: - :raises: exception.ImageNotFound, - exception.PhysicalResourceNameAmbiguity - ''' - try: - filters = {'name': image_identifier} - image_list = list(self.client().images.list(filters=filters)) - except exc.ClientException as ex: - raise exception.Error( - _("Error retrieving image list from glance: %s") % ex) - num_matches = len(image_list) - if num_matches == 0: - LOG.info(_LI("Image %s was not found in glance"), - image_identifier) - raise exception.ImageNotFound(image_name=image_identifier) - elif num_matches > 1: - LOG.info(_LI("Multiple images %s were found in glance with name"), - image_identifier) - raise exception.PhysicalResourceNameAmbiguity( - name=image_identifier) - else: - return image_list[0].id diff --git a/bilean/engine/clients/os/heat.py b/bilean/engine/clients/os/heat.py deleted file mode 100644 index f627b5c..0000000 --- a/bilean/engine/clients/os/heat.py +++ /dev/null @@ -1,65 +0,0 @@ -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -from bilean.engine.clients import client_plugin - -from heatclient import client as hc -from heatclient import exc - - -class HeatClientPlugin(client_plugin.ClientPlugin): - - exceptions_module = exc - - def _create(self): - args = { - 'auth_url': self.context.auth_url, - 'token': self.auth_token, - 'username': None, - 'password': None, - 'ca_file': self._get_client_option('heat', 'ca_file'), - 'cert_file': self._get_client_option('heat', 'cert_file'), - 'key_file': self._get_client_option('heat', 'key_file'), - 'insecure': self._get_client_option('heat', 'insecure') - } - - endpoint = self.get_heat_url() - if self._get_client_option('heat', 'url'): - # assume that the heat API URL is manually configured because - # it is not in the keystone catalog, so include the credentials - # for the standalone auth_password middleware - args['username'] = self.context.username - args['password'] = self.context.password - del(args['token']) - - return hc.Client('1', endpoint, **args) - - def is_not_found(self, ex): - return isinstance(ex, exc.HTTPNotFound) - - def is_over_limit(self, ex): - return isinstance(ex, exc.HTTPOverLimit) - - def is_conflict(self, ex): - return isinstance(ex, exc.HTTPConflict) - - def get_heat_url(self): - heat_url = self._get_client_option('heat', 'url') - if heat_url: - tenant_id = self.context.tenant_id - heat_url = heat_url % {'tenant_id': tenant_id} - else: - endpoint_type = self._get_client_option('heat', 'endpoint_type') - heat_url = self.url_for(service_type='orchestration', - endpoint_type=endpoint_type) - return heat_url diff --git a/bilean/engine/clients/os/keystone.py b/bilean/engine/clients/os/keystone.py deleted file mode 100644 index 2be0ad4..0000000 --- a/bilean/engine/clients/os/keystone.py +++ /dev/null @@ -1,44 +0,0 @@ -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -from bilean.engine.clients import client_plugin - -from oslo_config import cfg - -from keystoneclient import exceptions -from keystoneclient.v2_0 import client as keystone_client - - -class KeystoneClientPlugin(client_plugin.ClientPlugin): - - exceptions_module = exceptions - - @property - def kclient(self): - return keystone_client.Client( - username=cfg.CONF.authentication.service_username, - password=cfg.CONF.authentication.service_password, - tenant_name=cfg.CONF.authentication.service_project_name, - auth_url=cfg.CONF.authentication.auth_url) - - def _create(self): - return self.kclient - - def is_not_found(self, ex): - return isinstance(ex, exceptions.NotFound) - - def is_over_limit(self, ex): - return isinstance(ex, exceptions.RequestEntityTooLarge) - - def is_conflict(self, ex): - return isinstance(ex, exceptions.Conflict) diff --git a/bilean/engine/clients/os/neutron.py b/bilean/engine/clients/os/neutron.py deleted file mode 100644 index 74e4115..0000000 --- a/bilean/engine/clients/os/neutron.py +++ /dev/null @@ -1,119 +0,0 @@ -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -from bilean.common import exception -from bilean.engine.clients import client_plugin - -from oslo_utils import uuidutils - -from neutronclient.common import exceptions -from neutronclient.neutron import v2_0 as neutronV20 -from neutronclient.v2_0 import client as nc - - -class NeutronClientPlugin(client_plugin.ClientPlugin): - - exceptions_module = exceptions - - def _create(self): - - con = self.context - - endpoint_type = self._get_client_option('neutron', 'endpoint_type') - endpoint = self.url_for(service_type='network', - endpoint_type=endpoint_type) - - args = { - 'auth_url': con.auth_url, - 'service_type': 'network', - 'token': self.auth_token, - 'endpoint_url': endpoint, - 'endpoint_type': endpoint_type, - 'ca_cert': self._get_client_option('neutron', 'ca_file'), - 'insecure': self._get_client_option('neutron', 'insecure') - } - - return nc.Client(**args) - - def is_not_found(self, ex): - if isinstance(ex, (exceptions.NotFound, - exceptions.NetworkNotFoundClient, - exceptions.PortNotFoundClient)): - return True - return (isinstance(ex, exceptions.NeutronClientException) and - ex.status_code == 404) - - def is_conflict(self, ex): - if not isinstance(ex, exceptions.NeutronClientException): - return False - return ex.status_code == 409 - - def is_over_limit(self, ex): - if not isinstance(ex, exceptions.NeutronClientException): - return False - return ex.status_code == 413 - - def find_neutron_resource(self, props, key, key_type): - return neutronV20.find_resourceid_by_name_or_id( - self.client(), key_type, props.get(key)) - - def resolve_network(self, props, net_key, net_id_key): - if props.get(net_key): - props[net_id_key] = self.find_neutron_resource( - props, net_key, 'network') - props.pop(net_key) - return props[net_id_key] - - def resolve_subnet(self, props, subnet_key, subnet_id_key): - if props.get(subnet_key): - props[subnet_id_key] = self.find_neutron_resource( - props, subnet_key, 'subnet') - props.pop(subnet_key) - return props[subnet_id_key] - - def network_id_from_subnet_id(self, subnet_id): - subnet_info = self.client().show_subnet(subnet_id) - return subnet_info['subnet']['network_id'] - - def get_secgroup_uuids(self, security_groups): - '''Returns a list of security group UUIDs. - - Args: - security_groups: List of security group names or UUIDs - ''' - seclist = [] - all_groups = None - for sg in security_groups: - if uuidutils.is_uuid_like(sg): - seclist.append(sg) - else: - if not all_groups: - response = self.client().list_security_groups() - all_groups = response['security_groups'] - same_name_groups = [g for g in all_groups if g['name'] == sg] - groups = [g['id'] for g in same_name_groups] - if len(groups) == 0: - raise exception.PhysicalResourceNotFound(resource_id=sg) - elif len(groups) == 1: - seclist.append(groups[0]) - else: - # for admin roles, can get the other users' - # securityGroups, so we should match the tenant_id with - # the groups, and return the own one - own_groups = [g['id'] for g in same_name_groups - if g['tenant_id'] == self.context.tenant_id] - if len(own_groups) == 1: - seclist.append(own_groups[0]) - else: - raise exception.PhysicalResourceNameAmbiguity(name=sg) - return seclist diff --git a/bilean/engine/clients/os/nova.py b/bilean/engine/clients/os/nova.py deleted file mode 100644 index b36e507..0000000 --- a/bilean/engine/clients/os/nova.py +++ /dev/null @@ -1,294 +0,0 @@ -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -import collections -import json -import six - -from novaclient import client as nc -from novaclient import exceptions -from novaclient import shell as novashell - -from bilean.common import exception -from bilean.common.i18n import _ -from bilean.common.i18n import _LW -from bilean.engine.clients import client_plugin - -from oslo_log import log as logging - -LOG = logging.getLogger(__name__) - - -class NovaClientPlugin(client_plugin.ClientPlugin): - - deferred_server_statuses = ['BUILD', - 'HARD_REBOOT', - 'PASSWORD', - 'REBOOT', - 'RESCUE', - 'RESIZE', - 'REVERT_RESIZE', - 'SHUTOFF', - 'SUSPENDED', - 'VERIFY_RESIZE'] - - exceptions_module = exceptions - - def _create(self): - computeshell = novashell.OpenStackComputeShell() - extensions = computeshell._discover_extensions("1.1") - - endpoint_type = self._get_client_option('nova', 'endpoint_type') - args = { - 'project_id': self.context.tenant, - 'auth_url': self.context.auth_url, - 'service_type': 'compute', - 'username': None, - 'api_key': None, - 'extensions': extensions, - 'endpoint_type': endpoint_type, - 'http_log_debug': self._get_client_option('nova', - 'http_log_debug'), - 'cacert': self._get_client_option('nova', 'ca_file'), - 'insecure': self._get_client_option('nova', 'insecure') - } - - client = nc.Client(1.1, **args) - - management_url = self.url_for(service_type='compute', - endpoint_type=endpoint_type) - client.client.auth_token = self.auth_token - client.client.management_url = management_url - - return client - - def is_not_found(self, ex): - return isinstance(ex, exceptions.NotFound) - - def is_over_limit(self, ex): - return isinstance(ex, exceptions.OverLimit) - - def is_bad_request(self, ex): - return isinstance(ex, exceptions.BadRequest) - - def is_conflict(self, ex): - return isinstance(ex, exceptions.Conflict) - - def is_unprocessable_entity(self, ex): - http_status = (getattr(ex, 'http_status', None) or - getattr(ex, 'code', None)) - return (isinstance(ex, exceptions.ClientException) and - http_status == 422) - - def refresh_server(self, server): - '''Refresh server's attributes. - - Log warnings for non-critical API errors. - ''' - try: - server.get() - except exceptions.OverLimit as exc: - LOG.warn(_LW("Server %(name)s (%(id)s) received an OverLimit " - "response during server.get(): %(exception)s"), - {'name': server.name, - 'id': server.id, - 'exception': exc}) - except exceptions.ClientException as exc: - if ((getattr(exc, 'http_status', getattr(exc, 'code', None)) in - (500, 503))): - LOG.warn(_LW('Server "%(name)s" (%(id)s) received the ' - 'following exception during server.get(): ' - '%(exception)s'), - {'name': server.name, - 'id': server.id, - 'exception': exc}) - else: - raise - - def get_ip(self, server, net_type, ip_version): - """Return the server's IP of the given type and version.""" - if net_type in server.addresses: - for ip in server.addresses[net_type]: - if ip['version'] == ip_version: - return ip['addr'] - - def get_status(self, server): - '''Return the server's status. - - :param server: server object - :returns: status as a string - ''' - # Some clouds append extra (STATUS) strings to the status, strip it - return server.status.split('(')[0] - - def get_flavor_id(self, flavor): - '''Get the id for the specified flavor name. - - If the specified value is flavor id, just return it. - - :param flavor: the name of the flavor to find - :returns: the id of :flavor: - :raises: exception.FlavorMissing - ''' - flavor_id = None - flavor_list = self.client().flavors.list() - for o in flavor_list: - if o.name == flavor: - flavor_id = o.id - break - if o.id == flavor: - flavor_id = o.id - break - if flavor_id is None: - raise exception.FlavorMissing(flavor_id=flavor) - return flavor_id - - def get_keypair(self, key_name): - '''Get the public key specified by :key_name: - - :param key_name: the name of the key to look for - :returns: the keypair (name, public_key) for :key_name: - :raises: exception.UserKeyPairMissing - ''' - try: - return self.client().keypairs.get(key_name) - except exceptions.NotFound: - raise exception.UserKeyPairMissing(key_name=key_name) - - def delete_server(self, server): - '''Deletes a server and waits for it to disappear from Nova.''' - if not server: - return - try: - server.delete() - except Exception as exc: - self.ignore_not_found(exc) - return - - while True: - yield - - try: - self.refresh_server(server) - except Exception as exc: - self.ignore_not_found(exc) - break - else: - # Some clouds append extra (STATUS) strings to the status - short_server_status = server.status.split('(')[0] - if short_server_status in ("DELETED", "SOFT_DELETED"): - break - if short_server_status == "ERROR": - fault = getattr(server, 'fault', {}) - message = fault.get('message', 'Unknown') - code = fault.get('code') - errmsg = (_("Server %(name)s delete failed: (%(code)s) " - "%(message)s")) - raise exception.Error(errmsg % {"name": server.name, - "code": code, - "message": message}) - - def delete(self, server_id): - '''Delete a server by given server id''' - self.client().servers.delete(server_id) - - def resize(self, server, flavor, flavor_id): - """Resize the server and then call check_resize task to verify.""" - server.resize(flavor_id) - yield self.check_resize(server, flavor, flavor_id) - - def rename(self, server, name): - """Update the name for a server.""" - server.update(name) - - def check_resize(self, server, flavor, flavor_id): - """Verify that a resizing server is properly resized. - - If that's the case, confirm the resize, if not raise an error. - """ - self.refresh_server(server) - while server.status == 'RESIZE': - yield - self.refresh_server(server) - if server.status == 'VERIFY_RESIZE': - server.confirm_resize() - else: - raise exception.Error( - _("Resizing to '%(flavor)s' failed, status '%(status)s'") % - dict(flavor=flavor, status=server.status)) - - def rebuild(self, server, image_id, preserve_ephemeral=False): - """Rebuild the server and call check_rebuild to verify.""" - server.rebuild(image_id, preserve_ephemeral=preserve_ephemeral) - yield self.check_rebuild(server, image_id) - - def check_rebuild(self, server, image_id): - """Verify that a rebuilding server is rebuilt. - - Raise error if it ends up in an ERROR state. - """ - self.refresh_server(server) - while server.status == 'REBUILD': - yield - self.refresh_server(server) - if server.status == 'ERROR': - raise exception.Error( - _("Rebuilding server failed, status '%s'") % server.status) - - def meta_serialize(self, metadata): - """Serialize non-string metadata values before sending them to Nova.""" - if not isinstance(metadata, collections.Mapping): - raise exception.StackValidationFailed(message=_( - "nova server metadata needs to be a Map.")) - - return dict((key, (value if isinstance(value, - six.string_types) - else json.dumps(value)) - ) for (key, value) in metadata.items()) - - def meta_update(self, server, metadata): - """Delete/Add the metadata in nova as needed.""" - metadata = self.meta_serialize(metadata) - current_md = server.metadata - to_del = [key for key in current_md.keys() if key not in metadata] - client = self.client() - if len(to_del) > 0: - client.servers.delete_meta(server, to_del) - - client.servers.set_meta(server, metadata) - - def server_to_ipaddress(self, server): - '''Return the server's IP address, fetching it from Nova.''' - try: - server = self.client().servers.get(server) - except exceptions.NotFound as ex: - LOG.warn(_LW('Instance (%(server)s) not found: %(ex)s'), - {'server': server, 'ex': ex}) - else: - for n in server.networks: - if len(server.networks[n]) > 0: - return server.networks[n][0] - - def get_server(self, server): - try: - return self.client().servers.get(server) - except exceptions.NotFound as ex: - LOG.warn(_LW('Server (%(server)s) not found: %(ex)s'), - {'server': server, 'ex': ex}) - raise exception.ServerNotFound(server=server) - - def absolute_limits(self): - """Return the absolute limits as a dictionary.""" - limits = self.client().limits.get() - return dict([(limit.name, limit.value) - for limit in list(limits.absolute)]) diff --git a/bilean/engine/clients/os/sahara.py b/bilean/engine/clients/os/sahara.py deleted file mode 100644 index 21aef38..0000000 --- a/bilean/engine/clients/os/sahara.py +++ /dev/null @@ -1,51 +0,0 @@ -# Copyright (c) 2014 Mirantis Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or -# implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from bilean.engine.clients import client_plugin - -from saharaclient.api import base as sahara_base -from saharaclient import client as sahara_client - - -class SaharaClientPlugin(client_plugin.ClientPlugin): - - exceptions_module = sahara_base - - def _create(self): - con = self.context - endpoint_type = self._get_client_option('sahara', 'endpoint_type') - endpoint = self.url_for(service_type='data_processing', - endpoint_type=endpoint_type) - args = { - 'service_type': 'data_processing', - 'input_auth_token': self.auth_token, - 'auth_url': con.auth_url, - 'project_name': con.tenant, - 'sahara_url': endpoint - } - client = sahara_client.Client('1.1', **args) - return client - - def is_not_found(self, ex): - return (isinstance(ex, sahara_base.APIException) and - ex.error_code == 404) - - def is_over_limit(self, ex): - return (isinstance(ex, sahara_base.APIException) and - ex.error_code == 413) - - def is_conflict(self, ex): - return (isinstance(ex, sahara_base.APIException) and - ex.error_code == 409) diff --git a/bilean/engine/clients/os/trove.py b/bilean/engine/clients/os/trove.py deleted file mode 100644 index 77e37d3..0000000 --- a/bilean/engine/clients/os/trove.py +++ /dev/null @@ -1,77 +0,0 @@ -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -from troveclient import client as tc -from troveclient.openstack.common.apiclient import exceptions - -from bilean.common import exception -from bilean.engine.clients import client_plugin - - -class TroveClientPlugin(client_plugin.ClientPlugin): - - exceptions_module = exceptions - - def _create(self): - - con = self.context - endpoint_type = self._get_client_option('trove', 'endpoint_type') - args = { - 'service_type': 'database', - 'auth_url': con.auth_url or '', - 'proxy_token': con.auth_token, - 'username': None, - 'password': None, - 'cacert': self._get_client_option('trove', 'ca_file'), - 'insecure': self._get_client_option('trove', 'insecure'), - 'endpoint_type': endpoint_type - } - - client = tc.Client('1.0', **args) - management_url = self.url_for(service_type='database', - endpoint_type=endpoint_type) - client.client.auth_token = con.auth_token - client.client.management_url = management_url - - return client - - def is_not_found(self, ex): - return isinstance(ex, exceptions.NotFound) - - def is_over_limit(self, ex): - return isinstance(ex, exceptions.RequestEntityTooLarge) - - def is_conflict(self, ex): - return isinstance(ex, exceptions.Conflict) - - def get_flavor_id(self, flavor): - '''Get the id for the specified flavor name. - - If the specified value is flavor id, just return it. - - :param flavor: the name of the flavor to find - :returns: the id of :flavor: - :raises: exception.FlavorMissing - ''' - flavor_id = None - flavor_list = self.client().flavors.list() - for o in flavor_list: - if o.name == flavor: - flavor_id = o.id - break - if o.id == flavor: - flavor_id = o.id - break - if flavor_id is None: - raise exception.FlavorMissing(flavor_id=flavor) - return flavor_id diff --git a/bilean/engine/service.py b/bilean/engine/service.py index abf3e6f..acdeec7 100644 --- a/bilean/engine/service.py +++ b/bilean/engine/service.py @@ -11,6 +11,7 @@ # License for the specific language governing permissions and limitations # under the License. +import functools import six import socket @@ -38,6 +39,19 @@ from bilean.rules import base as rule_base LOG = logging.getLogger(__name__) +def request_context(func): + @functools.wraps(func) + def wrapped(self, ctx, *args, **kwargs): + if ctx is not None and not isinstance(ctx, + bilean_context.RequestContext): + ctx = bilean_context.RequestContext.from_dict(ctx.to_dict()) + try: + return func(self, ctx, *args, **kwargs) + except exception.BileanException: + raise oslo_messaging.rpc.dispatcher.ExpectedException() + return wrapped + + class EngineService(service.Service): """Manages the running instances from creation to destruction. @@ -64,7 +78,7 @@ class EngineService(service.Service): bilean_clients.initialise() if context is None: - self.context = bilean_context.get_service_context() + self.context = bilean_context.get_admin_context() def start(self): self.engine_id = socket.gethostname() @@ -109,7 +123,7 @@ class EngineService(service.Service): super(EngineService, self).stop() - @bilean_context.request_context + @request_context def user_list(self, cnxt, show_deleted=False, limit=None, marker=None, sort_keys=None, sort_dir=None, filters=None): @@ -132,7 +146,7 @@ class EngineService(service.Service): return user.to_dict() - @bilean_context.request_context + @request_context def user_get(self, cnxt, user_id): """Show detailed info about a specify user. @@ -141,7 +155,7 @@ class EngineService(service.Service): user = user_mod.User.load(cnxt, user_id=user_id, realtime=True) return user.to_dict() - @bilean_context.request_context + @request_context def user_recharge(self, cnxt, user_id, value): """Do recharge for specify user.""" user = user_mod.User.load(cnxt, user_id=user_id) @@ -156,7 +170,7 @@ class EngineService(service.Service): LOG.info(_LI('Deleging user: %s'), user_id) user_mod.User.delete(cnxt, user_id=user_id) - @bilean_context.request_context + @request_context def rule_create(self, cnxt, name, spec, metadata=None): if len(rule_base.Rule.load_all(cnxt, filters={'name': name})) > 0: msg = _("The rule (%(name)s) already exists." @@ -186,7 +200,7 @@ class EngineService(service.Service): {'name': name, 'id': rule.id}) return rule.to_dict() - @bilean_context.request_context + @request_context def rule_list(self, cnxt, limit=None, marker=None, sort_keys=None, sort_dir=None, filters=None, show_deleted=False): if limit is not None: @@ -203,21 +217,21 @@ class EngineService(service.Service): return [rule.to_dict() for rule in rules] - @bilean_context.request_context + @request_context def rule_get(self, cnxt, rule_id): rule = rule_base.Rule.load(cnxt, rule_id=rule_id) return rule.to_dict() - @bilean_context.request_context + @request_context def rule_update(self, cnxt, rule_id, values): return NotImplemented - @bilean_context.request_context + @request_context def rule_delete(self, cnxt, rule_id): LOG.info(_LI("Deleting rule: '%s'."), rule_id) rule_base.Rule.delete(cnxt, rule_id) - @bilean_context.request_context + @request_context def validate_creation(self, cnxt, resources): """Validate resources creation. @@ -271,7 +285,7 @@ class EngineService(service.Service): return resource.to_dict() - @bilean_context.request_context + @request_context def resource_list(self, cnxt, user_id=None, limit=None, marker=None, sort_keys=None, sort_dir=None, filters=None, tenant_safe=True, show_deleted=False): @@ -289,7 +303,7 @@ class EngineService(service.Service): show_deleted=show_deleted) return [r.to_dict() for r in resources] - @bilean_context.request_context + @request_context def resource_get(self, cnxt, resource_id): resource = resource_mod.Resource.load(cnxt, resource_id=resource_id) return resource.to_dict() @@ -323,7 +337,7 @@ class EngineService(service.Service): LOG.warn(_("Delete resource error %s"), ex) return - @bilean_context.request_context + @request_context def event_list(self, cnxt, user_id=None, limit=None, marker=None, sort_keys=None, sort_dir=None, filters=None, start_time=None, end_time=None, tenant_safe=True, @@ -345,7 +359,7 @@ class EngineService(service.Service): show_deleted=show_deleted) return [e.to_dict() for e in events] - @bilean_context.request_context + @request_context def policy_create(self, cnxt, name, rule_ids=None, metadata=None): """Create a new policy.""" if len(policy_mod.Policy.load_all(cnxt, filters={'name': name})) > 0: @@ -378,7 +392,7 @@ class EngineService(service.Service): LOG.info(_LI("Policy is created: %(id)s."), policy.id) return policy.to_dict() - @bilean_context.request_context + @request_context def policy_list(self, cnxt, limit=None, marker=None, sort_keys=None, sort_dir=None, filters=None, show_deleted=False): if limit is not None: @@ -395,12 +409,12 @@ class EngineService(service.Service): return [policy.to_dict() for policy in policies] - @bilean_context.request_context + @request_context def policy_get(self, cnxt, policy_id): policy = policy_mod.Policy.load(cnxt, policy_id=policy_id) return policy.to_dict() - @bilean_context.request_context + @request_context def policy_update(self, cnxt, policy_id, name=None, metadata=None, is_default=None): LOG.info(_LI("Updating policy: '%(id)s'"), {'id': policy_id}) @@ -437,15 +451,15 @@ class EngineService(service.Service): LOG.info(_LI("Policy '%(id)s' is updated."), {'id': policy_id}) return policy.to_dict() - @bilean_context.request_context + @request_context def policy_add_rule(self, cnxt, policy_id, rule_ids): return NotImplemented - @bilean_context.request_context + @request_context def policy_remove_rule(self, cnxt, policy_id, rule_ids): return NotImplemented - @bilean_context.request_context + @request_context def policy_delete(self, cnxt, policy_id): LOG.info(_LI("Deleting policy: '%s'."), policy_id) policy_mod.Policy.delete(cnxt, policy_id) diff --git a/bilean/engine/user.py b/bilean/engine/user.py index df35dab..2327603 100644 --- a/bilean/engine/user.py +++ b/bilean/engine/user.py @@ -11,10 +11,13 @@ # License for the specific language governing permissions and limitations # under the License. +import six + from bilean.common import exception from bilean.common.i18n import _ from bilean.common import utils from bilean.db import api as db_api +from bilean.drivers import base as driver_base from bilean.engine import event as event_mod from bilean.engine import resource as resource_mod @@ -77,15 +80,20 @@ class User(object): @classmethod def init_users(cls, context): """Init users from keystone.""" - k_client = context.clients.client('keystone') - tenants = k_client.tenants.list() - tenant_ids = [tenant.id for tenant in tenants] + keystoneclient = driver_base.BileanDriver().identity() + try: + projects = keystoneclient.project_list() + except exception.InternalError as ex: + LOG.exception(_('Failed in retrieving project list: %s'), + six.text_type(ex)) + return False + project_ids = [project.id for project in projects] users = cls.load_all(context) user_ids = [user.id for user in users] - for tid in tenant_ids: - if tid not in user_ids: - user = cls(tid, status=cls.INIT, + for pid in project_ids: + if pid not in user_ids: + user = cls(pid, status=cls.INIT, status_reason='Init from keystone') user.store(context) return True diff --git a/bilean/tests/drivers/__init__.py b/bilean/tests/drivers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/bilean/tests/drivers/test_driver.py b/bilean/tests/drivers/test_driver.py new file mode 100644 index 0000000..0b92dad --- /dev/null +++ b/bilean/tests/drivers/test_driver.py @@ -0,0 +1,47 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import mock +from oslo_config import cfg + +from bilean.drivers import base as driver_base +from bilean.engine import environment +from bilean.tests.common import base + + +class TestBileanDriver(base.BileanTestCase): + + def test_init_using_default_cloud_backend(self): + plugin1 = mock.Mock() + plugin1.compute = 'Compute1' + plugin1.network = 'Network1' + env = environment.global_env() + env.register_driver('cloud_backend_1', plugin1) + + # Using default cloud backend defined in configure file + cfg.CONF.set_override('cloud_backend', 'cloud_backend_1', + enforce_type=True) + bd = driver_base.BileanDriver() + self.assertEqual('Compute1', bd.compute) + self.assertEqual('Network1', bd.network) + + def test_init_using_specified_cloud_backend(self): + plugin2 = mock.Mock() + plugin2.compute = 'Compute2' + plugin2.network = 'Network2' + env = environment.global_env() + env.register_driver('cloud_backend_2', plugin2) + + # Using specified cloud backend + bd = driver_base.BileanDriver('cloud_backend_2') + self.assertEqual('Compute2', bd.compute) + self.assertEqual('Network2', bd.network) diff --git a/bilean/tests/drivers/test_sdk.py b/bilean/tests/drivers/test_sdk.py new file mode 100644 index 0000000..584fbde --- /dev/null +++ b/bilean/tests/drivers/test_sdk.py @@ -0,0 +1,211 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import mock +from openstack import connection +from openstack import profile +from oslo_serialization import jsonutils +from requests import exceptions as req_exc +import six + +from bilean.common import exception as bilean_exc +from bilean.drivers.openstack import sdk +from bilean.tests.common import base + + +class OpenStackSDKTest(base.BileanTestCase): + + def setUp(self): + super(OpenStackSDKTest, self).setUp() + + def test_parse_exception_http_exception_with_details(self): + details = jsonutils.dumps({ + 'error': { + 'code': 404, + 'message': 'Resource BAR is not found.' + } + }) + raw = sdk.exc.ResourceNotFound('A message', details, http_status=404) + ex = self.assertRaises(bilean_exc.InternalError, + sdk.parse_exception, raw) + + self.assertEqual(404, ex.code) + self.assertEqual('Resource BAR is not found.', six.text_type(ex)) + # key name is not 'error' case + details = jsonutils.dumps({ + 'forbidden': { + 'code': 403, + 'message': 'Quota exceeded for instances.' + } + }) + raw = sdk.exc.ResourceNotFound('A message', details, 403) + ex = self.assertRaises(bilean_exc.InternalError, + sdk.parse_exception, raw) + + self.assertEqual(403, ex.code) + self.assertEqual('Quota exceeded for instances.', six.text_type(ex)) + + def test_parse_exception_http_exception_no_details(self): + details = "An error message" + + raw = sdk.exc.ResourceNotFound('A message.', details, http_status=404) + ex = self.assertRaises(bilean_exc.InternalError, + sdk.parse_exception, raw) + + self.assertEqual(404, ex.code) + self.assertEqual('A message.', six.text_type(ex)) + + def test_parse_exception_http_exception_code_displaced(self): + details = jsonutils.dumps({ + 'code': 400, + 'error': { + 'message': 'Resource BAR is in error state.' + } + }) + + raw = sdk.exc.HttpException(message='A message.', details=details, + http_status=400) + ex = self.assertRaises(bilean_exc.InternalError, + sdk.parse_exception, raw) + + self.assertEqual(400, ex.code) + self.assertEqual('Resource BAR is in error state.', six.text_type(ex)) + + def test_parse_exception_sdk_exception(self): + raw = sdk.exc.InvalidResponse('INVALID') + + ex = self.assertRaises(bilean_exc.InternalError, + sdk.parse_exception, raw) + + self.assertEqual(500, ex.code) + self.assertEqual('InvalidResponse', six.text_type(ex)) + + def test_parse_exception_request_exception(self): + raw = req_exc.HTTPError(401, 'ERROR') + + ex = self.assertRaises(bilean_exc.InternalError, + sdk.parse_exception, raw) + + self.assertEqual(401, ex.code) + self.assertEqual('[Errno 401] ERROR', ex.message) + + def test_parse_exception_other_exceptions(self): + raw = Exception('Unknown Error') + + ex = self.assertRaises(bilean_exc.InternalError, + sdk.parse_exception, raw) + + self.assertEqual(500, ex.code) + self.assertEqual('Unknown Error', six.text_type(ex)) + + def test_translate_exception_wrapper(self): + + test_func = mock.Mock() + test_func.__name__ = 'test_func' + + res = sdk.translate_exception(test_func) + self.assertEqual('function', res.__class__.__name__) + + def test_translate_exception_with_exception(self): + + @sdk.translate_exception + def test_func(driver): + raise(Exception('test exception')) + + error = bilean_exc.InternalError(code=500, message='BOOM') + self.patchobject(sdk, 'parse_exception', side_effect=error) + ex = self.assertRaises(bilean_exc.InternalError, + test_func, mock.Mock()) + + self.assertEqual(500, ex.code) + self.assertEqual('BOOM', ex.message) + + @mock.patch.object(profile, 'Profile') + @mock.patch.object(connection, 'Connection') + def test_create_connection_token(self, mock_conn, mock_profile): + x_profile = mock.Mock() + mock_profile.return_value = x_profile + x_conn = mock.Mock() + mock_conn.return_value = x_conn + + res = sdk.create_connection({'token': 'TOKEN', 'foo': 'bar'}) + + self.assertEqual(x_conn, res) + mock_profile.assert_called_once_with() + x_profile.set_version.assert_called_once_with('identity', 'v3') + mock_conn.assert_called_once_with(profile=x_profile, + user_agent=sdk.USER_AGENT, + auth_plugin='token', + token='TOKEN', + foo='bar') + + @mock.patch.object(profile, 'Profile') + @mock.patch.object(connection, 'Connection') + def test_create_connection_password(self, mock_conn, mock_profile): + x_profile = mock.Mock() + mock_profile.return_value = x_profile + x_conn = mock.Mock() + mock_conn.return_value = x_conn + + res = sdk.create_connection({'user_id': '123', 'password': 'abc', + 'foo': 'bar'}) + + self.assertEqual(x_conn, res) + mock_profile.assert_called_once_with() + x_profile.set_version.assert_called_once_with('identity', 'v3') + mock_conn.assert_called_once_with(profile=x_profile, + user_agent=sdk.USER_AGENT, + auth_plugin='password', + user_id='123', + password='abc', + foo='bar') + + @mock.patch.object(profile, 'Profile') + @mock.patch.object(connection, 'Connection') + def test_create_connection_with_region(self, mock_conn, mock_profile): + x_profile = mock.Mock() + mock_profile.return_value = x_profile + x_conn = mock.Mock() + mock_conn.return_value = x_conn + + res = sdk.create_connection({'region_name': 'REGION_ONE'}) + + self.assertEqual(x_conn, res) + mock_profile.assert_called_once_with() + x_profile.set_region.assert_called_once_with(x_profile.ALL, + 'REGION_ONE') + mock_conn.assert_called_once_with(profile=x_profile, + user_agent=sdk.USER_AGENT, + auth_plugin='password') + + @mock.patch.object(profile, 'Profile') + @mock.patch.object(connection, 'Connection') + @mock.patch.object(sdk, 'parse_exception') + def test_create_connection_with_exception(self, mock_parse, mock_conn, + mock_profile): + x_profile = mock.Mock() + mock_profile.return_value = x_profile + ex_raw = Exception('Whatever') + mock_conn.side_effect = ex_raw + mock_parse.side_effect = bilean_exc.InternalError(code=123, + message='BOOM') + + ex = self.assertRaises(bilean_exc.InternalError, + sdk.create_connection) + + mock_profile.assert_called_once_with() + mock_conn.assert_called_once_with(profile=x_profile, + user_agent=sdk.USER_AGENT, + auth_plugin='password') + mock_parse.assert_called_once_with(ex_raw) + self.assertEqual(123, ex.code) + self.assertEqual('BOOM', ex.message) diff --git a/etc/bilean/api-paste.ini b/etc/bilean/api-paste.ini index 652333c..475019b 100644 --- a/etc/bilean/api-paste.ini +++ b/etc/bilean/api-paste.ini @@ -16,7 +16,7 @@ bilean.filter_factory = bilean.api.openstack:faultwrap_filter [filter:context] paste.filter_factory = bilean.common.wsgi:filter_factory -paste.filter_factory = bilean.common.context:ContextMiddleware_filter_factory +bilean.filter_factory = bilean.api.openstack:contextmiddleware_filter [filter:ssl] paste.filter_factory = bilean.common.wsgi:filter_factory diff --git a/requirements.txt b/requirements.txt index cf95028..858c5ea 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,7 +6,8 @@ apscheduler>=3.0.1 cryptography>=1.0 # Apache-2.0 eventlet>=0.17.4 jsonpath-rw-ext>=0.1.9 -keystonemiddleware>=4.0.0 +keystonemiddleware!=4.1.0,>=4.0.0 # Apache-2.0 +openstacksdk>=0.7.4 # Apache-2.0 oslo.config>=2.7.0 # Apache-2.0 oslo.context>=0.2.0 # Apache-2.0 oslo.db>=4.1.0 # Apache-2.0 diff --git a/setup.cfg b/setup.cfg index f9c7dc6..800356a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -35,15 +35,8 @@ oslo.config.opts = bilean.engine.scheduler = bilean.engine.scheduler:list_opts bilean.notification.converter = bilean.notification.converter:list_opts -bilean.clients = - ceilometer = bilean.engine.clients.os.ceilometer:CeilometerClientPlugin - cinder = bilean.engine.clients.os.cinder:CinderClientPlugin - glance = bilean.engine.clients.os.glance:GlanceClientPlugin - keystone = bilean.engine.clients.os.keystone:KeystoneClientPlugin - nova = bilean.engine.clients.os.nova:NovaClientPlugin - neutron = bilean.engine.clients.os.neutron:NeutronClientPlugin - trove = bilean.engine.clients.os.trove:TroveClientPlugin - sahara = bilean.engine.clients.os.sahara:SaharaClientPlugin +bilean.drivers = + openstack = bilean.drivers.openstack bilean.rules = os.nova.server = bilean.rules.os.nova.server:ServerRule diff --git a/tools/setup-service b/tools/setup-service index 2484b28..07963ea 100755 --- a/tools/setup-service +++ b/tools/setup-service @@ -35,12 +35,19 @@ if [[ -z $SERVICE_ID ]]; then exit fi -openstack endpoint create \ - --adminurl "http://$HOST:$PORT/v1" \ - --publicurl "http://$HOST:$PORT/v1" \ - --internalurl "http://$HOST:$PORT/v1" \ - --region RegionOne \ - bilean +#openstack endpoint create \ +# --adminurl "http://$HOST:$PORT/v1/\$(tenant_id)s" \ +# --publicurl "http://$HOST:$PORT/v1/\$(tenant_id)s" \ +# --internalurl "http://$HOST:$PORT/v1/\$(tenant_id)s" \ +# --region RegionOne \ +# bilean + +openstack endpoint create bilean admin \ + "http://$HOST:$PORT/v1/\$(tenant_id)s" --region RegionOne +openstack endpoint create bilean public \ + "http://$HOST:$PORT/v1/\$(tenant_id)s" --region RegionOne +openstack endpoint create bilean internal \ + "http://$HOST:$PORT/v1/\$(tenant_id)s" --region RegionOne openstack user create \ --password "$SVC_PASSWD" \