Use openstacksdk to communicate with other services
Change-Id: I083fc384b79665f9c64efa867a3b6d93f094dd0e
This commit is contained in:
parent
3120e19f61
commit
d6b03252ad
82
bilean/api/middleware/context.py
Normal file
82
bilean/api/middleware/context.py
Normal file
@ -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)
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
52
bilean/drivers/base.py
Normal file
52
bilean/drivers/base.py
Normal file
@ -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
|
20
bilean/drivers/openstack/__init__.py
Normal file
20
bilean/drivers/openstack/__init__.py
Normal file
@ -0,0 +1,20 @@
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
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
|
69
bilean/drivers/openstack/keystone_v3.py
Normal file
69
bilean/drivers/openstack/keystone_v3.py
Normal file
@ -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
|
125
bilean/drivers/openstack/neutron_v2.py
Normal file
125
bilean/drivers/openstack/neutron_v2.py
Normal file
@ -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
|
75
bilean/drivers/openstack/nova_v2.py
Normal file
75
bilean/drivers/openstack/nova_v2.py
Normal file
@ -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
|
114
bilean/drivers/openstack/sdk.py
Normal file
114
bilean/drivers/openstack/sdk.py
Normal file
@ -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
|
@ -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 _<name>() 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
|
@ -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
|
@ -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)
|
@ -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)
|
@ -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
|
@ -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
|
@ -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)
|
@ -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
|
@ -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)])
|
@ -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)
|
@ -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
|
@ -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)
|
||||
|
@ -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
|
||||
|
0
bilean/tests/drivers/__init__.py
Normal file
0
bilean/tests/drivers/__init__.py
Normal file
47
bilean/tests/drivers/test_driver.py
Normal file
47
bilean/tests/drivers/test_driver.py
Normal file
@ -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)
|
211
bilean/tests/drivers/test_sdk.py
Normal file
211
bilean/tests/drivers/test_sdk.py
Normal file
@ -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)
|
@ -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
|
||||
|
@ -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
|
||||
|
11
setup.cfg
11
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
|
||||
|
@ -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" \
|
||||
|
Loading…
Reference in New Issue
Block a user