312 lines
13 KiB
Plaintext
312 lines
13 KiB
Plaintext
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()
|