Rework server profile using new context building

Note this is a huge patch.

1) This patch reworks the context building logic for the nova server
   and heat stack profiles. We use a precise dictionary for connection
   setup, which include senlin credential and the trust cached. We don't
   use the huge context dictionary for this purpose.
2) This patch also fixed some cases where 'domain' is missing.
3) Added new exception types that will be used whenever backend objects
   are not found.
4) Revised sample nova spec, because we now support specifying image by
   name in addition to image id.
5) Reworked SDK exception translation module so that it can handle a
   different kind of exception (from Nova). Some 'print' calls are
   removed.
6) Reworked node_create logic so that 'user' and 'project' will be
   recorded when node is created.

Change-Id: If3160a5a8cc930dcebe91172da6a8bf22d20af47
This commit is contained in:
tengqm 2015-04-19 03:36:11 -04:00
parent 01ee398f27
commit 3e08b30cb9
9 changed files with 141 additions and 81 deletions

View File

@ -1,4 +1,4 @@
name: cirros_server
flavor: 1
image: ae83991b-6645-41c1-82c3-66be51a30c00
image: "cirros-0.3.2-x86_64-uec"
key_name: qmkey

View File

@ -272,6 +272,11 @@ class TrustNotFound(SenlinException):
msg_fmt = _("The trust for trustor (%(trustor)s) could not be found.")
class ResourceNotFound(SenlinException):
# Used when retrieving resources from other services
msg_fmt = _("The resource (%(resource)s) could not be found.")
class HTTPExceptionDisguise(Exception):
"""Disguises HTTP exceptions.

View File

@ -107,6 +107,7 @@ class KeystoneClient(base.DriverBase):
'trustee_user_id': trustee,
'project_id': project,
'impersonation': impersonation,
'allow_redelegation': True,
'roles': [{'name': role} for role in roles]
}
@ -119,7 +120,7 @@ class KeystoneClient(base.DriverBase):
return result
def get_service_credentials(**args):
def get_service_credentials(**kwargs):
'''Senlin service credential to use with Keystone.
:param args: An additional keyword argument list that can be used
@ -130,7 +131,9 @@ def get_service_credentials(**args):
'user_name': CONF.keystone_authtoken.admin_user,
'password': CONF.keystone_authtoken.admin_password,
'auth_url': CONF.keystone_authtoken.auth_uri,
'project_name': CONF.keystone_authtoken.admin_tenant_name
'project_name': CONF.keystone_authtoken.admin_tenant_name,
'user_domain_name': 'Default',
'project_domain_name': 'Default',
}
creds.update(**args)
creds.update(**kwargs)
return creds

View File

@ -13,6 +13,7 @@
import time
from oslo_config import cfg
from oslo_log import log
from openstack.compute.v2 import flavor
from openstack.compute.v2 import image
@ -27,13 +28,15 @@ from senlin.common import exception
from senlin.drivers import base
from senlin.drivers.openstack import sdk
LOG = log.getLogger(__name__)
class NovaClient(base.DriverBase):
'''Nova V2 driver.'''
def __init__(self, context):
def __init__(self, params):
# TODO(anyone): Need to make the user_preferences work here.
conn = sdk.create_connection(context)
conn = sdk.create_connection(params)
self.session = conn.session
self.auth = self.session.authenticator
@ -87,15 +90,19 @@ class NovaClient(base.DriverBase):
except sdk.exc.HttpException as ex:
raise ex
def image_get_by_name(self, name):
imgs = [img for img in self.image_list(name=name)]
if len(imgs) == 0:
raise exception.ResourceNotFound(resource=name)
return imgs[0]
def image_list(self, **params):
try:
return image.Image.list(self.session, **params)
except sdk.exc.HttpException as ex:
raise ex
def image_update(self, **params):
raise NotImplemented
def image_delete(self, **params):
obj = image.Image.new(**params)
try:
@ -185,6 +192,10 @@ class NovaClient(base.DriverBase):
raise ex
def server_delete(self, **params):
def _is_not_found(ex):
parsed = sdk.parse_exception(ex)
return isinstance(parsed, sdk.HTTPNotFound)
timeout = cfg.CONF.default_action_timeout
if 'timeout' in params:
timeout = params.pop('timeout')
@ -193,17 +204,16 @@ class NovaClient(base.DriverBase):
try:
obj.delete(self.session)
except sdk.exc.HttpException as ex:
sdk.ignore_not_found(ex)
if _is_not_found(ex):
return
total_sleep = 0
while total_sleep < timeout:
try:
obj.get(self.session)
except sdk.exc.HttpException as ex:
parsed = sdk.exc.parse_exception(ex)
if isinstance(parsed, sdk.exc.HTTPNotFound):
return
raise ex
if not _is_not_found(ex):
raise ex
time.sleep(5)
total_sleep += 5

View File

@ -25,9 +25,7 @@ from senlin.common import context
from senlin.common.i18n import _
USER_AGENT = 'senlin'
exc = exceptions
verbose = False
class BaseException(Exception):
@ -60,14 +58,9 @@ class HTTPException(BaseException):
def __str__(self):
message = self.error['error'].get('message', 'Internal Error')
if verbose:
traceback = self.error['error'].get('traceback', '')
return (_('ERROR: %(message)s\n%(traceback)s') %
{'message': message, 'traceback': traceback})
else:
code = self.error['error'].get('code', 'Unknown')
return _('ERROR(%(code)s): %(message)s') % {'code': code,
'message': message}
code = self.error['error'].get('code', 'Unknown')
return _('ERROR(%(code)s): %(message)s') % {'code': code,
'message': message}
class ClientError(HTTPException):
@ -135,36 +128,41 @@ def parse_exception(ex):
:param details: details of the exception.
'''
if isinstance(ex, exc.HttpException):
record = jsonutils.loads(ex.details)
code = 500
message = _('Unknown exception: %s') % ex
if isinstance(ex, exceptions.HttpException):
try:
data = jsonutils.loads(ex.details)
code = data['error'].get('code', None)
if code is None:
code = data['code']
message = data['error']['message']
except ValueError:
# Some exceptions don't have details record, we try make a guess
code = ex.status_code
message = ex.message
elif isinstance(ex, reqexc.RequestException):
# Exceptions that are not captured by SDK
if isinstance(ex.message, list):
msg = ex.message[0]
else:
msg = ex.message
code = ex.message[1].errno
record = {
'error': {
'code': code,
'message': ex.message[0],
}
}
else:
print(_('Unknown exception: %s') % ex)
return
message = msg
try:
code = record['error'].get('code', None)
if code is None:
code = record['code']
record['error']['code'] = code
except KeyError as err:
print(_('Malformed exception record, missing field "%s"') % err)
print(_('Original error record: %s') % record)
return
body = {
'error': {
'code': code,
'message': message,
}
}
if code in _EXCEPTION_MAP:
inst = _EXCEPTION_MAP.get(code)
return inst(record)
return inst(body)
else:
return HTTPException(record)
return HTTPException(body)
def ignore_not_found(ex):
@ -180,11 +178,12 @@ def create_connection(ctx):
'auth_url': ctx.auth_url,
'domain_id': ctx.domain,
'project_id': ctx.project,
'project_domain_id': ctx.project_domain,
'user_domain_id': ctx.user_domain,
'project_domain_name': ctx.project_domain_name,
'user_domain_name': ctx.user_domain_name,
'username': ctx.user_name,
'user_id': ctx.user,
'password': ctx.password,
'trust_id': ctx.trusts,
'token': ctx.auth_token,
# 'auth_plugin': args.auth_plugin,
# 'verify': OS_CACERT, TLS certificate to verify remote server

View File

@ -423,10 +423,10 @@ class EngineService(service.Service):
timeout = utils.parse_int_param(consts.CLUSTER_TIMEOUT, timeout)
LOG.info(_LI('Creating cluster %s'), name)
ctx = context.to_dict()
kwargs = {
'user': ctx.get('user', ''),
'project': ctx.get('project', ''),
'user': context.user,
'project': context.project,
'domain': context.domain,
'parent': parent,
'timeout': timeout,
'tags': tags
@ -732,9 +732,16 @@ class EngineService(service.Service):
LOG.info(_LI('Creating node %s'), name)
# Create a node instance
tags = tags or {}
kwargs = {
'user': context.user,
'project': context.project,
'domain': context.domain,
'role': role,
'tags': tags or {}
}
node = node_mod.Node(name, node_profile.id, cluster_id, context,
role=role, tags=tags)
**kwargs)
node.store(context)
action = action_mod.Action(context, 'NODE_CREATE',

View File

@ -17,6 +17,7 @@ from senlin.common import context
from senlin.common import exception
from senlin.common import schema
from senlin.db import api as db_api
from senlin.drivers.openstack import keystone_v3 as keystoneclient
from senlin.engine import environment
LOG = logging.getLogger(__name__)
@ -145,6 +146,28 @@ class Profile(object):
'''Validate the schema and the data provided.'''
self.spec_data.validate()
def _get_connection_params(self, obj):
cred = db_api.cred_get(self.context, obj.user, obj.project)
if cred is None:
raise exception.TrustNotFound(trustor=obj.user)
trust_id = cred.cred['openstack']['trust']
ctx = keystoneclient.get_service_credentials()
params = {
'auth_url': ctx['auth_url'],
'user_name': ctx['user_name'],
'user_domain_name': ctx['user_domain_name'],
'password': ctx['password'],
'project_id': obj.project,
'trusts': trust_id,
}
profile_context = self.spec_data[self.CONTEXT]
if profile_context is not None and len(profile_context) > 0:
# We don't know what will happen, it is completely left to users.
params.update(profile_context)
return params
def do_create(self, obj):
'''For subclass to override.'''

View File

@ -14,7 +14,6 @@ import six
from oslo_log import log as logging
from senlin.common import context
from senlin.common import exception
from senlin.common.i18n import _
from senlin.common import schema
@ -77,23 +76,16 @@ class StackProfile(base.Profile):
def __init__(self, ctx, type_name, name, **kwargs):
super(StackProfile, self).__init__(ctx, type_name, name, **kwargs)
# a stack profile may have its own context customization
stack_context = self.spec_data[self.CONTEXT]
if stack_context is not None:
ctx_dict = ctx.to_dict()
ctx_dict.update(stack_context)
self.context = context.RequestContext.from_dict(ctx_dict)
self.hc = None
self.stack_id = None
def heat(self):
def heat(self, obj):
'''Construct heat client using the combined context.'''
if self.hc:
return self.hc
self.hc = heatclient.HeatClient(self.context)
params = self._get_connection_params(obj)
self.hc = heatclient.HeatClient(params)
return self.hc
def do_validate(self, obj):
@ -109,7 +101,7 @@ class StackProfile(base.Profile):
'environment': self.spec_data[self.ENVIRONMENT],
}
try:
self.heat().stacks.validate(**kwargs)
self.heat(obj).stacks.validate(**kwargs)
except Exception as ex:
msg = _('Failed validate stack template due to '
'"%s"') % six.text_type(ex)
@ -118,7 +110,7 @@ class StackProfile(base.Profile):
return True
def _check_action_complete(self, obj, action):
stack = self.heat().stack_get(id=self.stack_id)
stack = self.heat(obj).stack_get(id=self.stack_id)
status = stack.stack_status.split('_', 1)
if status[0] == action:
@ -157,7 +149,7 @@ class StackProfile(base.Profile):
}
LOG.info('Creating stack: %s' % kwargs)
stack = self.heat().stack_create(**kwargs)
stack = self.heat(obj).stack_create(**kwargs)
self.stack_id = stack.id
# Wait for action to complete/fail
@ -170,7 +162,7 @@ class StackProfile(base.Profile):
self.stack_id = obj.physical_id
try:
self.heat().stack_delete(id=self.stack_id)
self.heat(obj).stack_delete(id=self.stack_id)
except Exception as ex:
raise ex
@ -203,7 +195,7 @@ class StackProfile(base.Profile):
'environment': new_profile.spec_data[new_profile.ENVIRONMENT],
}
self.heat().stack_update(**fields)
self.heat(obj).stack_update(**fields)
# Wait for action to complete/fail
while not self._check_action_complete(obj, 'UPDATE'):

View File

@ -11,7 +11,9 @@
# under the License.
from oslo_log import log as logging
import six
from senlin.common import exception
from senlin.common.i18n import _
from senlin.common import schema
from senlin.drivers.openstack import nova_v2 as novaclient
@ -24,13 +26,13 @@ class ServerProfile(base.Profile):
'''Profile for an OpenStack Nova server.'''
KEYS = (
ADMIN_PASS, AUTO_DISK_CONFIG, AVAILABILITY_ZONE,
CONTEXT, ADMIN_PASS, AUTO_DISK_CONFIG, AVAILABILITY_ZONE,
BLOCK_DEVICE_MAPPING, # BLOCK_DEVICE_MAPPING_V2,
CONFIG_DRIVE, FLAVOR, IMAGE, KEY_NAME, METADATA,
NAME, NETWORKS, PERSONALITY, SECURITY_GROUPS,
TIMEOUT, USER_DATA,
) = (
'adminPass', 'auto_disk_config', 'availability_zone',
'context', 'adminPass', 'auto_disk_config', 'availability_zone',
'block_device_mapping',
# 'block_device_mapping_v2',
'config_drive', 'flavor', 'image', 'key_name', 'metadata',
@ -58,6 +60,9 @@ class ServerProfile(base.Profile):
)
spec_schema = {
CONTEXT: schema.Map(
_('Customized security context for operating servers.'),
),
ADMIN_PASS: schema.String(
_('Password for the administrator account.'),
),
@ -160,13 +165,18 @@ class ServerProfile(base.Profile):
self._nc = None
self.server_id = None
def nova(self):
'''Construct heat client using the combined context.'''
def nova(self, obj):
'''Construct nova client based on object.
:param obj: Object for which the client is created. It is expected to
be None when retrieving an existing client. When creating
a client, it contains the user and project to be used.
'''
if self._nc is not None:
return self._nc
self._nc = novaclient.NovaClient(self.context)
params = self._get_connection_params(obj)
self._nc = novaclient.NovaClient(params)
return self._nc
def do_validate(self, obj):
@ -177,7 +187,6 @@ class ServerProfile(base.Profile):
def do_create(self, obj):
'''Create a server using the given profile.'''
kwargs = {}
for k in self.KEYS:
if k in self.spec_data:
@ -185,18 +194,29 @@ class ServerProfile(base.Profile):
kwargs[k] = self.spec_data[k]
if self.IMAGE in self.spec_data:
image = self.nova().image_get(id=self.spec_data[self.IMAGE])
name_or_id = self.spec_data[self.IMAGE]
try:
image = self.nova(obj).image_get(id=name_or_id)
except Exception:
# could be a image name
pass
try:
image = self.nova(obj).image_get_by_name(name_or_id)
except Exception:
raise exception.ResourceNotFound(resource=name_or_id)
kwargs[self.IMAGE] = image
if self.FLAVOR in self.spec_data:
flavor = self.nova().flavor_get(id=self.spec_data[self.FLAVOR])
flavor = self.nova(obj).flavor_get(id=self.spec_data[self.FLAVOR])
kwargs[self.FLAVOR] = flavor
if obj.name is not None:
kwargs[self.NAME] = obj.name
LOG.info('Creating server: %s' % kwargs)
server = self.nova().server_create(**kwargs)
server = self.nova(obj).server_create(**kwargs)
self.server_id = server.id
return server.id
@ -205,8 +225,9 @@ class ServerProfile(base.Profile):
self.server_id = obj.physical_id
try:
self.nova().server_delete(id=self.server_id)
self.nova(obj).server_delete(id=self.server_id)
except Exception as ex:
LOG.error('error: %s' % six.text_type(ex))
raise ex
return True
@ -224,7 +245,7 @@ class ServerProfile(base.Profile):
# TODO(anyone): Validate the new profile
# TODO(anyone): Do update based on the fields provided.
# self.nova().server_update(**fields)
# self.nova(obj).server_update(**fields)
return True
def do_check(self, obj):