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:
parent
01ee398f27
commit
3e08b30cb9
@ -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
|
||||
|
@ -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.
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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',
|
||||
|
@ -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.'''
|
||||
|
||||
|
@ -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'):
|
||||
|
@ -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):
|
||||
|
Loading…
Reference in New Issue
Block a user