diff --git a/examples/profiles/nova_server.spec b/examples/profiles/nova_server.spec index 35b85523a..f210e301c 100644 --- a/examples/profiles/nova_server.spec +++ b/examples/profiles/nova_server.spec @@ -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 diff --git a/senlin/common/exception.py b/senlin/common/exception.py index 16078f161..3dcbde063 100644 --- a/senlin/common/exception.py +++ b/senlin/common/exception.py @@ -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. diff --git a/senlin/drivers/openstack/keystone_v3.py b/senlin/drivers/openstack/keystone_v3.py index 3ed2562f6..fce751e9b 100644 --- a/senlin/drivers/openstack/keystone_v3.py +++ b/senlin/drivers/openstack/keystone_v3.py @@ -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 diff --git a/senlin/drivers/openstack/nova_v2.py b/senlin/drivers/openstack/nova_v2.py index 76fc33e65..11e6f4766 100644 --- a/senlin/drivers/openstack/nova_v2.py +++ b/senlin/drivers/openstack/nova_v2.py @@ -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 diff --git a/senlin/drivers/openstack/sdk.py b/senlin/drivers/openstack/sdk.py index 81f73034e..61245b0be 100644 --- a/senlin/drivers/openstack/sdk.py +++ b/senlin/drivers/openstack/sdk.py @@ -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 diff --git a/senlin/engine/service.py b/senlin/engine/service.py index d62335234..aadb59645 100644 --- a/senlin/engine/service.py +++ b/senlin/engine/service.py @@ -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', diff --git a/senlin/profiles/base.py b/senlin/profiles/base.py index 6cc132630..f3f2b0983 100644 --- a/senlin/profiles/base.py +++ b/senlin/profiles/base.py @@ -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.''' diff --git a/senlin/profiles/os/heat/stack.py b/senlin/profiles/os/heat/stack.py index 8e552c43f..5bd7b377a 100644 --- a/senlin/profiles/os/heat/stack.py +++ b/senlin/profiles/os/heat/stack.py @@ -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'): diff --git a/senlin/profiles/os/nova/server.py b/senlin/profiles/os/nova/server.py index 384324927..d6d30b5f0 100644 --- a/senlin/profiles/os/nova/server.py +++ b/senlin/profiles/os/nova/server.py @@ -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):