diff --git a/ironic/api/config.py b/ironic/api/config.py index 38938c1..18c82fd 100644 --- a/ironic/api/config.py +++ b/ironic/api/config.py @@ -31,7 +31,8 @@ app = { '/', '/v1', '/v1/drivers/[a-z_]*/vendor_passthru/lookup', - '/v1/nodes/[a-z0-9\-]+/vendor_passthru/heartbeat' + '/v1/nodes/[a-z0-9\-]+/vendor_passthru/heartbeat', + '/v1/nodes/[a-z0-9\-]+/vendor_passthru/pass_deploy_info', ], } diff --git a/ironic/api/controllers/v1/node.py b/ironic/api/controllers/v1/node.py index ce48e09..0df9a3f 100644 --- a/ironic/api/controllers/v1/node.py +++ b/ironic/api/controllers/v1/node.py @@ -381,13 +381,17 @@ class NodeStatesController(rest.RestController): rpc_node = api_utils.get_rpc_node(node_ident) topic = pecan.request.rpcapi.get_topic_for(rpc_node) + driver = api_utils.get_driver_by_name(rpc_node.driver) + driver_can_terminate = (driver and + driver.deploy.can_terminate_deployment) # Normally, we let the task manager recognize and deal with # NodeLocked exceptions. However, that isn't done until the RPC calls # below. In order to main backward compatibility with our API HTTP # response codes, we have this check here to deal with cases where # a node is already being operated on (DEPLOYING or such) and we # want to continue returning 409. Without it, we'd return 400. - if rpc_node.reservation: + if (not (target == ir_states.DELETED and driver_can_terminate) and + rpc_node.reservation): raise exception.NodeLocked(node=rpc_node.uuid, host=rpc_node.reservation) diff --git a/ironic/api/controllers/v1/utils.py b/ironic/api/controllers/v1/utils.py index 6132e12..91ca0f2 100644 --- a/ironic/api/controllers/v1/utils.py +++ b/ironic/api/controllers/v1/utils.py @@ -19,6 +19,7 @@ from oslo_utils import uuidutils import pecan import wsme +from ironic.common import driver_factory from ironic.common import exception from ironic.common.i18n import _ from ironic.common import utils @@ -102,3 +103,12 @@ def is_valid_node_name(name): :returns: True if the name is valid, False otherwise. """ return utils.is_hostname_safe(name) and (not uuidutils.is_uuid_like(name)) + + +def get_driver_by_name(driver_name): + _driver_factory = driver_factory.DriverFactory() + try: + driver = _driver_factory[driver_name] + return driver.obj + except Exception: + return None diff --git a/ironic/common/context.py b/ironic/common/context.py index aaeffb3..d167e26 100644 --- a/ironic/common/context.py +++ b/ironic/common/context.py @@ -63,5 +63,4 @@ class RequestContext(context.RequestContext): @classmethod def from_dict(cls, values): values.pop('user', None) - values.pop('tenant', None) return cls(**values) diff --git a/ironic/common/states.py b/ironic/common/states.py index 7ebd052..df30c2f 100644 --- a/ironic/common/states.py +++ b/ironic/common/states.py @@ -218,6 +218,9 @@ machine.add_state(INSPECTFAIL, target=MANAGEABLE, **watchers) # A deployment may fail machine.add_transition(DEPLOYING, DEPLOYFAIL, 'fail') +# A deployment may be terminated +machine.add_transition(DEPLOYING, DELETING, 'delete') + # A failed deployment may be retried # ironic/conductor/manager.py:do_node_deploy() machine.add_transition(DEPLOYFAIL, DEPLOYING, 'rebuild') diff --git a/ironic/common/swift.py b/ironic/common/swift.py index a4444e2..4cc36c4 100644 --- a/ironic/common/swift.py +++ b/ironic/common/swift.py @@ -23,6 +23,7 @@ from swiftclient import utils as swift_utils from ironic.common import exception from ironic.common.i18n import _ from ironic.common import keystone +from ironic.common import utils from ironic.openstack.common import log as logging swift_opts = [ @@ -36,6 +37,13 @@ swift_opts = [ CONF = cfg.CONF CONF.register_opts(swift_opts, group='swift') +CONF.import_opt('swift_endpoint_url', + 'ironic.common.glance_service.v2.image_service', + group='glance') +CONF.import_opt('swift_api_version', + 'ironic.common.glance_service.v2.image_service', + group='glance') + CONF.import_opt('admin_user', 'keystonemiddleware.auth_token', group='keystone_authtoken') CONF.import_opt('admin_tenant_name', 'keystonemiddleware.auth_token', @@ -60,7 +68,9 @@ class SwiftAPI(object): tenant_name=CONF.keystone_authtoken.admin_tenant_name, key=CONF.keystone_authtoken.admin_password, auth_url=CONF.keystone_authtoken.auth_uri, - auth_version=CONF.keystone_authtoken.auth_version): + auth_version=CONF.keystone_authtoken.auth_version, + preauthtoken=None, + preauthtenant=None): """Constructor for creating a SwiftAPI object. :param user: the name of the user for Swift account @@ -68,15 +78,40 @@ class SwiftAPI(object): :param key: the 'password' or key to authenticate with :param auth_url: the url for authentication :param auth_version: the version of api to use for authentication + :param preauthtoken: authentication token (if you have already + authenticated) note authurl/user/key/tenant_name + are not required when specifying preauthtoken + :param preauthtenant a tenant that will be accessed using the + preauthtoken """ - auth_url = keystone.get_keystone_url(auth_url, auth_version) - params = {'retries': CONF.swift.swift_max_retries, - 'insecure': CONF.keystone_authtoken.insecure, - 'user': user, - 'tenant_name': tenant_name, - 'key': key, - 'authurl': auth_url, - 'auth_version': auth_version} + params = { + 'retries': CONF.swift.swift_max_retries, + 'insecure': CONF.keystone_authtoken.insecure + } + + if preauthtoken: + # Determining swift url for the user's tenant account. + tenant_id = utils.get_tenant_id(tenant_name=preauthtenant) + url = "{endpoint}/{api_ver}/AUTH_{tenant}".format( + endpoint=CONF.glance.swift_endpoint_url, + api_ver=CONF.glance.swift_api_version, + tenant=tenant_id + ) + # authurl/user/key/tenant_name are not required when specifying + # preauthtoken + params.update({ + 'preauthtoken': preauthtoken, + 'preauthurl': url + }) + else: + auth_url = keystone.get_keystone_url(auth_url, auth_version) + params.update({ + 'user': user, + 'tenant_name': tenant_name, + 'key': key, + 'authurl': auth_url, + 'auth_version': auth_version + }) self.connection = swift_client.Connection(**params) @@ -128,8 +163,8 @@ class SwiftAPI(object): operation = _("head account") raise exception.SwiftOperationError(operation=operation, error=e) - - storage_url, token = self.connection.get_auth() + storage_url = (self.connection.os_options.get('object_storage_url') or + self.connection.get_auth()[0]) parse_result = parse.urlparse(storage_url) swift_object_path = '/'.join((parse_result.path, container, object)) temp_url_key = account_info['x-account-meta-temp-url-key'] @@ -186,3 +221,23 @@ class SwiftAPI(object): except swift_exceptions.ClientException as e: operation = _("post object") raise exception.SwiftOperationError(operation=operation, error=e) + + def get_object(self, container, object, object_headers=None, + chunk_size=None): + """Get Swift object. + + :param container: The name of the container in which Swift object + is placed. + :param object: The name of the object in Swift + :param object_headers: the headers for the object to pass to Swift + :param chunk_size: size of the chunk used read to read from response + :returns: Tuple (body, headers) + :raises: SwiftOperationError, if operation with Swift fails. + """ + try: + return self.connection.get_object(container, object, + headers=object_headers, + resp_chunk_size=chunk_size) + except swift_exceptions.ClientException as e: + operation = _("get object") + raise exception.SwiftOperationError(operation=operation, error=e) diff --git a/ironic/common/utils.py b/ironic/common/utils.py index 3633f82..4d1ca28 100644 --- a/ironic/common/utils.py +++ b/ironic/common/utils.py @@ -38,6 +38,7 @@ from ironic.common import exception from ironic.common.i18n import _ from ironic.common.i18n import _LE from ironic.common.i18n import _LW +from ironic.common import keystone from ironic.openstack.common import log as logging utils_opts = [ @@ -536,3 +537,8 @@ def dd(src, dst, *args): def is_http_url(url): url = url.lower() return url.startswith('http://') or url.startswith('https://') + + +def get_tenant_id(tenant_name): + ksclient = keystone._get_ksclient() + return ksclient.tenants.find(name=tenant_name).to_dict()['id'] diff --git a/ironic/conductor/manager.py b/ironic/conductor/manager.py index c2b75bc..53f516b 100644 --- a/ironic/conductor/manager.py +++ b/ironic/conductor/manager.py @@ -766,6 +766,11 @@ class ConductorManager(periodic_task.PeriodicTasks): """ LOG.debug("RPC do_node_tear_down called for node %s." % node_id) + with task_manager.acquire(context, node_id, shared=True) as task: + if (task.node.provision_state == states.DEPLOYING and + task.driver.deploy.can_terminate_deployment): + task.driver.deploy.terminate_deployment(task) + with task_manager.acquire(context, node_id, shared=False) as task: try: # NOTE(ghe): Valid power driver values are needed to perform diff --git a/ironic/drivers/base.py b/ironic/drivers/base.py index e0685d0..d1fa4bc 100644 --- a/ironic/drivers/base.py +++ b/ironic/drivers/base.py @@ -318,6 +318,13 @@ class DeployInterface(BaseInterface): """ pass + def terminate_deployment(self, *args, **kwargs): + pass + + @property + def can_terminate_deployment(self): + return False + @six.add_metaclass(abc.ABCMeta) class PowerInterface(BaseInterface): diff --git a/ironic/drivers/modules/image_cache.py b/ironic/drivers/modules/image_cache.py index d7b27c0..eb3ec55 100644 --- a/ironic/drivers/modules/image_cache.py +++ b/ironic/drivers/modules/image_cache.py @@ -25,9 +25,9 @@ import uuid from oslo_concurrency import lockutils from oslo_config import cfg +from oslo_utils import uuidutils from ironic.common import exception -from ironic.common.glance_service import service_utils from ironic.common.i18n import _LI from ironic.common.i18n import _LW from ironic.common import images @@ -100,15 +100,15 @@ class ImageCache(object): # TODO(ghe): have hard links and counts the same behaviour in all fs - # NOTE(vdrok): File name is converted to UUID if it's not UUID already, - # so that two images with same file names do not collide - if service_utils.is_glance_image(href): - master_file_name = service_utils.parse_image_ref(href)[0] + if uuidutils.is_uuid_like(href): + master_file_name = href + elif (self._image_service and + hasattr(self._image_service, 'get_image_unique_id')): + master_file_name = self._image_service.get_image_unique_id(href) else: - # NOTE(vdrok): Doing conversion of href in case it's unicode - # string, UUID cannot be generated for unicode strings on python 2. master_file_name = str(uuid.uuid5(uuid.NAMESPACE_URL, href.encode('utf-8'))) + master_path = os.path.join(self.master_dir, master_file_name) if CONF.parallel_image_downloads: diff --git a/ironic/tests/test_swift.py b/ironic/tests/test_swift.py index 9daa06e..aaa1b7c 100644 --- a/ironic/tests/test_swift.py +++ b/ironic/tests/test_swift.py @@ -113,6 +113,7 @@ class SwiftTestCase(base.TestCase): connection_obj_mock.get_auth.return_value = auth head_ret_val = {'x-account-meta-temp-url-key': 'secretkey'} connection_obj_mock.head_account.return_value = head_ret_val + connection_obj_mock.os_options = {} gen_temp_url_mock.return_value = 'temp-url-path' temp_url_returned = swiftapi.get_temp_url('container', 'object', 10) connection_obj_mock.get_auth.assert_called_once_with()