summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMark Goddard <mgoddard@cray.com>2016-04-04 13:23:39 +0300
committerOleksandr Berezovskyi <berezovskyi.oleksandr@gmail.com>2016-04-18 17:13:06 +0300
commit210a6383e4543653c09a6c360c2665612cfcd691 (patch)
treee4baaa19f8e0f0fa4f6491e504abea334fca16e8
parentde9c84dc806fdc26bc2b4b9503748a97dc0f8b22 (diff)
Fixed compatibiliy with stable/libertyliberty-eol
Notes
Notes (review): Code-Review+2: Max Lobur <max_lobur@outlook.com> Code-Review+2: Oleksandr Berezovskyi <berezovskyi.oleksandr@gmail.com> Verified+1: Oleksandr Berezovskyi <berezovskyi.oleksandr@gmail.com> Workflow+1: Oleksandr Berezovskyi <berezovskyi.oleksandr@gmail.com> Verified+1: Max Lobur <max_lobur@outlook.com> Verified+2: Jenkins Submitted-by: Jenkins Submitted-at: Mon, 18 Apr 2016 22:14:20 +0000 Reviewed-on: https://review.openstack.org/301024 Project: openstack/bareon-ironic Branch: refs/heads/master
-rw-r--r--bareon_ironic/bareon.py10
-rw-r--r--bareon_ironic/modules/bareon_base.py6
-rw-r--r--bareon_ironic/modules/bareon_utils.py2
-rw-r--r--bareon_ironic/modules/resources/actions.py2
-rw-r--r--bareon_ironic/modules/resources/image_service.py2
-rw-r--r--bareon_ironic/modules/resources/resources.py4
-rw-r--r--bareon_ironic/modules/resources/rsync.py3
-rw-r--r--patches/patch-ironic-stable-kilo311
-rw-r--r--patches/patch-ironic-stable-liberty508
-rw-r--r--patches/patch-nova-stable-liberty (renamed from patches/patch-nova-stable-kilo)497
10 files changed, 693 insertions, 652 deletions
diff --git a/bareon_ironic/bareon.py b/bareon_ironic/bareon.py
index b966907..aeb25a1 100644
--- a/bareon_ironic/bareon.py
+++ b/bareon_ironic/bareon.py
@@ -14,7 +14,7 @@
14# limitations under the License. 14# limitations under the License.
15 15
16from ironic.drivers import base 16from ironic.drivers import base
17from ironic.drivers.modules import discoverd 17from ironic.drivers.modules import inspector
18from ironic.drivers.modules import ipmitool 18from ironic.drivers.modules import ipmitool
19from ironic.drivers.modules import ssh 19from ironic.drivers.modules import ssh
20 20
@@ -39,7 +39,7 @@ class BareonSwiftAndIPMIToolDriver(base.BaseDriver):
39 self.deploy = bareon_swift.BareonSwiftDeploy() 39 self.deploy = bareon_swift.BareonSwiftDeploy()
40 self.management = ipmitool.IPMIManagement() 40 self.management = ipmitool.IPMIManagement()
41 self.vendor = bareon_swift.BareonSwiftVendor() 41 self.vendor = bareon_swift.BareonSwiftVendor()
42 self.inspect = discoverd.DiscoverdInspect.create_if_enabled( 42 self.inspect = inspector.Inspector.create_if_enabled(
43 'BareonSwiftAndIPMIToolDriver') 43 'BareonSwiftAndIPMIToolDriver')
44 44
45 45
@@ -61,7 +61,7 @@ class BareonSwiftAndSSHDriver(base.BaseDriver):
61 self.deploy = bareon_swift.BareonSwiftDeploy() 61 self.deploy = bareon_swift.BareonSwiftDeploy()
62 self.management = ssh.SSHManagement() 62 self.management = ssh.SSHManagement()
63 self.vendor = bareon_swift.BareonSwiftVendor() 63 self.vendor = bareon_swift.BareonSwiftVendor()
64 self.inspect = discoverd.DiscoverdInspect.create_if_enabled( 64 self.inspect = inspector.Inspector.create_if_enabled(
65 'BareonSwiftAndSSHDriver') 65 'BareonSwiftAndSSHDriver')
66 66
67 67
@@ -82,7 +82,7 @@ class BareonRsyncAndIPMIToolDriver(base.BaseDriver):
82 self.deploy = bareon_rsync.BareonRsyncDeploy() 82 self.deploy = bareon_rsync.BareonRsyncDeploy()
83 self.management = ipmitool.IPMIManagement() 83 self.management = ipmitool.IPMIManagement()
84 self.vendor = bareon_rsync.BareonRsyncVendor() 84 self.vendor = bareon_rsync.BareonRsyncVendor()
85 self.inspect = discoverd.DiscoverdInspect.create_if_enabled( 85 self.inspect = inspector.Inspector.create_if_enabled(
86 'BareonRsyncAndIPMIToolDriver') 86 'BareonRsyncAndIPMIToolDriver')
87 87
88 88
@@ -104,5 +104,5 @@ class BareonRsyncAndSSHDriver(base.BaseDriver):
104 self.deploy = bareon_rsync.BareonRsyncDeploy() 104 self.deploy = bareon_rsync.BareonRsyncDeploy()
105 self.management = ssh.SSHManagement() 105 self.management = ssh.SSHManagement()
106 self.vendor = bareon_rsync.BareonRsyncVendor() 106 self.vendor = bareon_rsync.BareonRsyncVendor()
107 self.inspect = discoverd.DiscoverdInspect.create_if_enabled( 107 self.inspect = inspector.Inspector.create_if_enabled(
108 'BareonRsyncAndSSHDriver') 108 'BareonRsyncAndSSHDriver')
diff --git a/bareon_ironic/modules/bareon_base.py b/bareon_ironic/modules/bareon_base.py
index c37c482..26e5bb4 100644
--- a/bareon_ironic/modules/bareon_base.py
+++ b/bareon_ironic/modules/bareon_base.py
@@ -27,6 +27,9 @@ import six
27from oslo_concurrency import processutils 27from oslo_concurrency import processutils
28from oslo_config import cfg 28from oslo_config import cfg
29from oslo_utils import excutils 29from oslo_utils import excutils
30from oslo_utils import fileutils
31from oslo_log import log
32from oslo_service import loopingcall
30 33
31from ironic.common import boot_devices 34from ironic.common import boot_devices
32from ironic.common import dhcp_factory 35from ironic.common import dhcp_factory
@@ -45,9 +48,6 @@ from ironic.drivers import base
45from ironic.drivers.modules import deploy_utils 48from ironic.drivers.modules import deploy_utils
46from ironic.drivers.modules import image_cache 49from ironic.drivers.modules import image_cache
47from ironic.objects import node as db_node 50from ironic.objects import node as db_node
48from ironic.openstack.common import fileutils
49from ironic.openstack.common import log
50from ironic.openstack.common import loopingcall
51 51
52from bareon_ironic.modules import bareon_exception 52from bareon_ironic.modules import bareon_exception
53from bareon_ironic.modules import bareon_utils 53from bareon_ironic.modules import bareon_utils
diff --git a/bareon_ironic/modules/bareon_utils.py b/bareon_ironic/modules/bareon_utils.py
index dbd41fb..cb12d8b 100644
--- a/bareon_ironic/modules/bareon_utils.py
+++ b/bareon_ironic/modules/bareon_utils.py
@@ -23,6 +23,7 @@ import tempfile
23import six 23import six
24from oslo_concurrency import processutils 24from oslo_concurrency import processutils
25from oslo_config import cfg 25from oslo_config import cfg
26from oslo_log import log as logging
26from oslo_utils import strutils 27from oslo_utils import strutils
27 28
28from ironic.common import dhcp_factory 29from ironic.common import dhcp_factory
@@ -30,7 +31,6 @@ from ironic.common import exception
30from ironic.common import keystone 31from ironic.common import keystone
31from ironic.common import utils 32from ironic.common import utils
32from ironic.common.i18n import _, _LW 33from ironic.common.i18n import _, _LW
33from ironic.openstack.common import log as logging
34 34
35LOG = logging.getLogger(__name__) 35LOG = logging.getLogger(__name__)
36CONF = cfg.CONF 36CONF = cfg.CONF
diff --git a/bareon_ironic/modules/resources/actions.py b/bareon_ironic/modules/resources/actions.py
index 6afdb1f..d9ff17d 100644
--- a/bareon_ironic/modules/resources/actions.py
+++ b/bareon_ironic/modules/resources/actions.py
@@ -23,9 +23,9 @@ import tempfile
23 23
24from oslo_concurrency import processutils 24from oslo_concurrency import processutils
25from oslo_config import cfg 25from oslo_config import cfg
26from oslo_log import log
26 27
27from ironic.common import exception 28from ironic.common import exception
28from ironic.openstack.common import log
29 29
30from bareon_ironic.modules import bareon_utils 30from bareon_ironic.modules import bareon_utils
31from bareon_ironic.modules.resources import resources 31from bareon_ironic.modules.resources import resources
diff --git a/bareon_ironic/modules/resources/image_service.py b/bareon_ironic/modules/resources/image_service.py
index 6842e54..d64632e 100644
--- a/bareon_ironic/modules/resources/image_service.py
+++ b/bareon_ironic/modules/resources/image_service.py
@@ -22,6 +22,7 @@ import uuid
22 22
23from oslo_config import cfg 23from oslo_config import cfg
24from oslo_concurrency import processutils 24from oslo_concurrency import processutils
25from oslo_log import log as logging
25from oslo_utils import uuidutils 26from oslo_utils import uuidutils
26import requests 27import requests
27import six 28import six
@@ -29,7 +30,6 @@ import six.moves.urllib.parse as urlparse
29 30
30from ironic.common import exception 31from ironic.common import exception
31from ironic.common.i18n import _ 32from ironic.common.i18n import _
32from ironic.openstack.common import log as logging
33from ironic.common import image_service 33from ironic.common import image_service
34from ironic.common import keystone 34from ironic.common import keystone
35from ironic.common import utils 35from ironic.common import utils
diff --git a/bareon_ironic/modules/resources/resources.py b/bareon_ironic/modules/resources/resources.py
index d3952e9..cf2abda 100644
--- a/bareon_ironic/modules/resources/resources.py
+++ b/bareon_ironic/modules/resources/resources.py
@@ -19,14 +19,14 @@ import os
19 19
20from oslo_config import cfg 20from oslo_config import cfg
21from oslo_serialization import jsonutils 21from oslo_serialization import jsonutils
22from oslo_utils import fileutils
23from oslo_log import log
22from six.moves.urllib import parse 24from six.moves.urllib import parse
23 25
24from ironic.common import exception 26from ironic.common import exception
25from ironic.common import utils 27from ironic.common import utils
26from ironic.common.i18n import _ 28from ironic.common.i18n import _
27from ironic.drivers.modules import image_cache 29from ironic.drivers.modules import image_cache
28from ironic.openstack.common import fileutils
29from ironic.openstack.common import log
30 30
31from bareon_ironic.modules import bareon_exception 31from bareon_ironic.modules import bareon_exception
32from bareon_ironic.modules import bareon_utils 32from bareon_ironic.modules import bareon_utils
diff --git a/bareon_ironic/modules/resources/rsync.py b/bareon_ironic/modules/resources/rsync.py
index ebe4cb4..b3b2828 100644
--- a/bareon_ironic/modules/resources/rsync.py
+++ b/bareon_ironic/modules/resources/rsync.py
@@ -17,8 +17,7 @@
17import os 17import os
18 18
19from oslo_config import cfg 19from oslo_config import cfg
20 20from oslo_log import log
21from ironic.openstack.common import log
22 21
23rsync_opts = [ 22rsync_opts = [
24 cfg.StrOpt('rsync_server', 23 cfg.StrOpt('rsync_server',
diff --git a/patches/patch-ironic-stable-kilo b/patches/patch-ironic-stable-kilo
deleted file mode 100644
index de3ccc8..0000000
--- a/patches/patch-ironic-stable-kilo
+++ /dev/null
@@ -1,311 +0,0 @@
1diff --git a/ironic/api/config.py b/ironic/api/config.py
2index 38938c1..18c82fd 100644
3--- a/ironic/api/config.py
4+++ b/ironic/api/config.py
5@@ -31,7 +31,8 @@ app = {
6 '/',
7 '/v1',
8 '/v1/drivers/[a-z_]*/vendor_passthru/lookup',
9- '/v1/nodes/[a-z0-9\-]+/vendor_passthru/heartbeat'
10+ '/v1/nodes/[a-z0-9\-]+/vendor_passthru/heartbeat',
11+ '/v1/nodes/[a-z0-9\-]+/vendor_passthru/pass_deploy_info',
12 ],
13 }
14
15diff --git a/ironic/api/controllers/v1/node.py b/ironic/api/controllers/v1/node.py
16index ce48e09..0df9a3f 100644
17--- a/ironic/api/controllers/v1/node.py
18+++ b/ironic/api/controllers/v1/node.py
19@@ -381,13 +381,17 @@ class NodeStatesController(rest.RestController):
20 rpc_node = api_utils.get_rpc_node(node_ident)
21 topic = pecan.request.rpcapi.get_topic_for(rpc_node)
22
23+ driver = api_utils.get_driver_by_name(rpc_node.driver)
24+ driver_can_terminate = (driver and
25+ driver.deploy.can_terminate_deployment)
26 # Normally, we let the task manager recognize and deal with
27 # NodeLocked exceptions. However, that isn't done until the RPC calls
28 # below. In order to main backward compatibility with our API HTTP
29 # response codes, we have this check here to deal with cases where
30 # a node is already being operated on (DEPLOYING or such) and we
31 # want to continue returning 409. Without it, we'd return 400.
32- if rpc_node.reservation:
33+ if (not (target == ir_states.DELETED and driver_can_terminate) and
34+ rpc_node.reservation):
35 raise exception.NodeLocked(node=rpc_node.uuid,
36 host=rpc_node.reservation)
37
38diff --git a/ironic/api/controllers/v1/utils.py b/ironic/api/controllers/v1/utils.py
39index 6132e12..91ca0f2 100644
40--- a/ironic/api/controllers/v1/utils.py
41+++ b/ironic/api/controllers/v1/utils.py
42@@ -19,6 +19,7 @@ from oslo_utils import uuidutils
43 import pecan
44 import wsme
45
46+from ironic.common import driver_factory
47 from ironic.common import exception
48 from ironic.common.i18n import _
49 from ironic.common import utils
50@@ -102,3 +103,12 @@ def is_valid_node_name(name):
51 :returns: True if the name is valid, False otherwise.
52 """
53 return utils.is_hostname_safe(name) and (not uuidutils.is_uuid_like(name))
54+
55+
56+def get_driver_by_name(driver_name):
57+ _driver_factory = driver_factory.DriverFactory()
58+ try:
59+ driver = _driver_factory[driver_name]
60+ return driver.obj
61+ except Exception:
62+ return None
63diff --git a/ironic/common/context.py b/ironic/common/context.py
64index aaeffb3..d167e26 100644
65--- a/ironic/common/context.py
66+++ b/ironic/common/context.py
67@@ -63,5 +63,4 @@ class RequestContext(context.RequestContext):
68 @classmethod
69 def from_dict(cls, values):
70 values.pop('user', None)
71- values.pop('tenant', None)
72 return cls(**values)
73diff --git a/ironic/common/states.py b/ironic/common/states.py
74index 7ebd052..df30c2f 100644
75--- a/ironic/common/states.py
76+++ b/ironic/common/states.py
77@@ -218,6 +218,9 @@ machine.add_state(INSPECTFAIL, target=MANAGEABLE, **watchers)
78 # A deployment may fail
79 machine.add_transition(DEPLOYING, DEPLOYFAIL, 'fail')
80
81+# A deployment may be terminated
82+machine.add_transition(DEPLOYING, DELETING, 'delete')
83+
84 # A failed deployment may be retried
85 # ironic/conductor/manager.py:do_node_deploy()
86 machine.add_transition(DEPLOYFAIL, DEPLOYING, 'rebuild')
87diff --git a/ironic/common/swift.py b/ironic/common/swift.py
88index a4444e2..4cc36c4 100644
89--- a/ironic/common/swift.py
90+++ b/ironic/common/swift.py
91@@ -23,6 +23,7 @@ from swiftclient import utils as swift_utils
92 from ironic.common import exception
93 from ironic.common.i18n import _
94 from ironic.common import keystone
95+from ironic.common import utils
96 from ironic.openstack.common import log as logging
97
98 swift_opts = [
99@@ -36,6 +37,13 @@ swift_opts = [
100 CONF = cfg.CONF
101 CONF.register_opts(swift_opts, group='swift')
102
103+CONF.import_opt('swift_endpoint_url',
104+ 'ironic.common.glance_service.v2.image_service',
105+ group='glance')
106+CONF.import_opt('swift_api_version',
107+ 'ironic.common.glance_service.v2.image_service',
108+ group='glance')
109+
110 CONF.import_opt('admin_user', 'keystonemiddleware.auth_token',
111 group='keystone_authtoken')
112 CONF.import_opt('admin_tenant_name', 'keystonemiddleware.auth_token',
113@@ -60,7 +68,9 @@ class SwiftAPI(object):
114 tenant_name=CONF.keystone_authtoken.admin_tenant_name,
115 key=CONF.keystone_authtoken.admin_password,
116 auth_url=CONF.keystone_authtoken.auth_uri,
117- auth_version=CONF.keystone_authtoken.auth_version):
118+ auth_version=CONF.keystone_authtoken.auth_version,
119+ preauthtoken=None,
120+ preauthtenant=None):
121 """Constructor for creating a SwiftAPI object.
122
123 :param user: the name of the user for Swift account
124@@ -68,15 +78,40 @@ class SwiftAPI(object):
125 :param key: the 'password' or key to authenticate with
126 :param auth_url: the url for authentication
127 :param auth_version: the version of api to use for authentication
128+ :param preauthtoken: authentication token (if you have already
129+ authenticated) note authurl/user/key/tenant_name
130+ are not required when specifying preauthtoken
131+ :param preauthtenant a tenant that will be accessed using the
132+ preauthtoken
133 """
134- auth_url = keystone.get_keystone_url(auth_url, auth_version)
135- params = {'retries': CONF.swift.swift_max_retries,
136- 'insecure': CONF.keystone_authtoken.insecure,
137- 'user': user,
138- 'tenant_name': tenant_name,
139- 'key': key,
140- 'authurl': auth_url,
141- 'auth_version': auth_version}
142+ params = {
143+ 'retries': CONF.swift.swift_max_retries,
144+ 'insecure': CONF.keystone_authtoken.insecure
145+ }
146+
147+ if preauthtoken:
148+ # Determining swift url for the user's tenant account.
149+ tenant_id = utils.get_tenant_id(tenant_name=preauthtenant)
150+ url = "{endpoint}/{api_ver}/AUTH_{tenant}".format(
151+ endpoint=CONF.glance.swift_endpoint_url,
152+ api_ver=CONF.glance.swift_api_version,
153+ tenant=tenant_id
154+ )
155+ # authurl/user/key/tenant_name are not required when specifying
156+ # preauthtoken
157+ params.update({
158+ 'preauthtoken': preauthtoken,
159+ 'preauthurl': url
160+ })
161+ else:
162+ auth_url = keystone.get_keystone_url(auth_url, auth_version)
163+ params.update({
164+ 'user': user,
165+ 'tenant_name': tenant_name,
166+ 'key': key,
167+ 'authurl': auth_url,
168+ 'auth_version': auth_version
169+ })
170
171 self.connection = swift_client.Connection(**params)
172
173@@ -128,8 +163,8 @@ class SwiftAPI(object):
174 operation = _("head account")
175 raise exception.SwiftOperationError(operation=operation,
176 error=e)
177-
178- storage_url, token = self.connection.get_auth()
179+ storage_url = (self.connection.os_options.get('object_storage_url') or
180+ self.connection.get_auth()[0])
181 parse_result = parse.urlparse(storage_url)
182 swift_object_path = '/'.join((parse_result.path, container, object))
183 temp_url_key = account_info['x-account-meta-temp-url-key']
184@@ -186,3 +221,23 @@ class SwiftAPI(object):
185 except swift_exceptions.ClientException as e:
186 operation = _("post object")
187 raise exception.SwiftOperationError(operation=operation, error=e)
188+
189+ def get_object(self, container, object, object_headers=None,
190+ chunk_size=None):
191+ """Get Swift object.
192+
193+ :param container: The name of the container in which Swift object
194+ is placed.
195+ :param object: The name of the object in Swift
196+ :param object_headers: the headers for the object to pass to Swift
197+ :param chunk_size: size of the chunk used read to read from response
198+ :returns: Tuple (body, headers)
199+ :raises: SwiftOperationError, if operation with Swift fails.
200+ """
201+ try:
202+ return self.connection.get_object(container, object,
203+ headers=object_headers,
204+ resp_chunk_size=chunk_size)
205+ except swift_exceptions.ClientException as e:
206+ operation = _("get object")
207+ raise exception.SwiftOperationError(operation=operation, error=e)
208diff --git a/ironic/common/utils.py b/ironic/common/utils.py
209index 3633f82..4d1ca28 100644
210--- a/ironic/common/utils.py
211+++ b/ironic/common/utils.py
212@@ -38,6 +38,7 @@ from ironic.common import exception
213 from ironic.common.i18n import _
214 from ironic.common.i18n import _LE
215 from ironic.common.i18n import _LW
216+from ironic.common import keystone
217 from ironic.openstack.common import log as logging
218
219 utils_opts = [
220@@ -536,3 +537,8 @@ def dd(src, dst, *args):
221 def is_http_url(url):
222 url = url.lower()
223 return url.startswith('http://') or url.startswith('https://')
224+
225+
226+def get_tenant_id(tenant_name):
227+ ksclient = keystone._get_ksclient()
228+ return ksclient.tenants.find(name=tenant_name).to_dict()['id']
229diff --git a/ironic/conductor/manager.py b/ironic/conductor/manager.py
230index c2b75bc..53f516b 100644
231--- a/ironic/conductor/manager.py
232+++ b/ironic/conductor/manager.py
233@@ -766,6 +766,11 @@ class ConductorManager(periodic_task.PeriodicTasks):
234 """
235 LOG.debug("RPC do_node_tear_down called for node %s." % node_id)
236
237+ with task_manager.acquire(context, node_id, shared=True) as task:
238+ if (task.node.provision_state == states.DEPLOYING and
239+ task.driver.deploy.can_terminate_deployment):
240+ task.driver.deploy.terminate_deployment(task)
241+
242 with task_manager.acquire(context, node_id, shared=False) as task:
243 try:
244 # NOTE(ghe): Valid power driver values are needed to perform
245diff --git a/ironic/drivers/base.py b/ironic/drivers/base.py
246index e0685d0..d1fa4bc 100644
247--- a/ironic/drivers/base.py
248+++ b/ironic/drivers/base.py
249@@ -318,6 +318,13 @@ class DeployInterface(BaseInterface):
250 """
251 pass
252
253+ def terminate_deployment(self, *args, **kwargs):
254+ pass
255+
256+ @property
257+ def can_terminate_deployment(self):
258+ return False
259+
260
261 @six.add_metaclass(abc.ABCMeta)
262 class PowerInterface(BaseInterface):
263diff --git a/ironic/drivers/modules/image_cache.py b/ironic/drivers/modules/image_cache.py
264index d7b27c0..eb3ec55 100644
265--- a/ironic/drivers/modules/image_cache.py
266+++ b/ironic/drivers/modules/image_cache.py
267@@ -25,9 +25,9 @@ import uuid
268
269 from oslo_concurrency import lockutils
270 from oslo_config import cfg
271+from oslo_utils import uuidutils
272
273 from ironic.common import exception
274-from ironic.common.glance_service import service_utils
275 from ironic.common.i18n import _LI
276 from ironic.common.i18n import _LW
277 from ironic.common import images
278@@ -100,15 +100,15 @@ class ImageCache(object):
279
280 # TODO(ghe): have hard links and counts the same behaviour in all fs
281
282- # NOTE(vdrok): File name is converted to UUID if it's not UUID already,
283- # so that two images with same file names do not collide
284- if service_utils.is_glance_image(href):
285- master_file_name = service_utils.parse_image_ref(href)[0]
286+ if uuidutils.is_uuid_like(href):
287+ master_file_name = href
288+ elif (self._image_service and
289+ hasattr(self._image_service, 'get_image_unique_id')):
290+ master_file_name = self._image_service.get_image_unique_id(href)
291 else:
292- # NOTE(vdrok): Doing conversion of href in case it's unicode
293- # string, UUID cannot be generated for unicode strings on python 2.
294 master_file_name = str(uuid.uuid5(uuid.NAMESPACE_URL,
295 href.encode('utf-8')))
296+
297 master_path = os.path.join(self.master_dir, master_file_name)
298
299 if CONF.parallel_image_downloads:
300diff --git a/ironic/tests/test_swift.py b/ironic/tests/test_swift.py
301index 9daa06e..aaa1b7c 100644
302--- a/ironic/tests/test_swift.py
303+++ b/ironic/tests/test_swift.py
304@@ -113,6 +113,7 @@ class SwiftTestCase(base.TestCase):
305 connection_obj_mock.get_auth.return_value = auth
306 head_ret_val = {'x-account-meta-temp-url-key': 'secretkey'}
307 connection_obj_mock.head_account.return_value = head_ret_val
308+ connection_obj_mock.os_options = {}
309 gen_temp_url_mock.return_value = 'temp-url-path'
310 temp_url_returned = swiftapi.get_temp_url('container', 'object', 10)
311 connection_obj_mock.get_auth.assert_called_once_with()
diff --git a/patches/patch-ironic-stable-liberty b/patches/patch-ironic-stable-liberty
new file mode 100644
index 0000000..ee2e6b7
--- /dev/null
+++ b/patches/patch-ironic-stable-liberty
@@ -0,0 +1,508 @@
1diff --git a/ironic/api/controllers/v1/node.py b/ironic/api/controllers/v1/node.py
2index d95298f..1b99c68 100644
3--- a/ironic/api/controllers/v1/node.py
4+++ b/ironic/api/controllers/v1/node.py
5@@ -452,8 +452,13 @@ class NodeStatesController(rest.RestController):
6 raise exception.NodeInMaintenance(op=_('provisioning'),
7 node=rpc_node.uuid)
8
9+ driver = api_utils.get_driver_by_name(rpc_node.driver)
10+ driver_can_terminate = (driver and
11+ driver.deploy.can_terminate_deployment)
12+
13 m = ir_states.machine.copy()
14 m.initialize(rpc_node.provision_state)
15+
16 if not m.is_actionable_event(ir_states.VERBS.get(target, target)):
17 # Normally, we let the task manager recognize and deal with
18 # NodeLocked exceptions. However, that isn't done until the RPC
19@@ -470,6 +475,16 @@ class NodeStatesController(rest.RestController):
20 action=target, node=rpc_node.uuid,
21 state=rpc_node.provision_state)
22
23+ # Note(obereozvskyi): we need to check weather driver supports deploy
24+ # terminating
25+ if (m.current_state == ir_states.DEPLOYING and
26+ target == ir_states.DELETED and
27+ not driver_can_terminate):
28+
29+ raise exception.InvalidStateRequested(
30+ action=target, node=rpc_node.uuid,
31+ state=rpc_node.provision_state)
32+
33 if configdrive and target != ir_states.ACTIVE:
34 msg = (_('Adding a config drive is only supported when setting '
35 'provision state to %s') % ir_states.ACTIVE)
36diff --git a/ironic/api/controllers/v1/utils.py b/ironic/api/controllers/v1/utils.py
37index 538ca45..3715d41 100644
38--- a/ironic/api/controllers/v1/utils.py
39+++ b/ironic/api/controllers/v1/utils.py
40@@ -23,6 +23,7 @@ from webob.static import FileIter
41 import wsme
42
43 from ironic.api.controllers.v1 import versions
44+from ironic.common import driver_factory
45 from ironic.common import exception
46 from ironic.common.i18n import _
47 from ironic.common import states
48@@ -109,7 +110,16 @@ def is_valid_node_name(name):
49 :param: name: the node name to check.
50 :returns: True if the name is valid, False otherwise.
51 """
52- return is_valid_logical_name(name) and not uuidutils.is_uuid_like(name)
53+ return utils.is_hostname_safe(name) and (not uuidutils.is_uuid_like(name))
54+
55+
56+def get_driver_by_name(driver_name):
57+ _driver_factory = driver_factory.DriverFactory()
58+ try:
59+ driver = _driver_factory[driver_name]
60+ return driver.obj
61+ except Exception:
62+ return None
63
64
65 def is_valid_logical_name(name):
66diff --git a/ironic/common/context.py b/ironic/common/context.py
67index ccd2222..b8186c9 100644
68--- a/ironic/common/context.py
69+++ b/ironic/common/context.py
70@@ -65,5 +65,4 @@ class RequestContext(context.RequestContext):
71 @classmethod
72 def from_dict(cls, values):
73 values.pop('user', None)
74- values.pop('tenant', None)
75 return cls(**values)
76diff --git a/ironic/common/images.py b/ironic/common/images.py
77index 5b00e65..28e6bd7 100644
78--- a/ironic/common/images.py
79+++ b/ironic/common/images.py
80@@ -328,16 +328,17 @@ def convert_image(source, dest, out_format, run_as_root=False):
81 utils.execute(*cmd, run_as_root=run_as_root)
82
83
84-def fetch(context, image_href, path, force_raw=False):
85+def fetch(context, image_href, path, force_raw=False, image_service=None):
86 # TODO(vish): Improve context handling and add owner and auth data
87 # when it is added to glance. Right now there is no
88 # auth checking in glance, so we assume that access was
89 # checked before we got here.
90- image_service = service.get_image_service(image_href,
91- context=context)
92- LOG.debug("Using %(image_service)s to download image %(image_href)s." %
93- {'image_service': image_service.__class__,
94- 'image_href': image_href})
95+ if not image_service:
96+ image_service = service.get_image_service(image_href,
97+ context=context)
98+ LOG.debug("Using %(image_service)s to download image %(image_href)s." %
99+ {'image_service': image_service.__class__,
100+ 'image_href': image_href})
101
102 with fileutils.remove_path_on_error(path):
103 with open(path, "wb") as image_file:
104diff --git a/ironic/common/states.py b/ironic/common/states.py
105index e61c807..2523a7f 100644
106--- a/ironic/common/states.py
107+++ b/ironic/common/states.py
108@@ -245,6 +245,9 @@ machine.add_state(INSPECTFAIL, target=MANAGEABLE, **watchers)
109 # A deployment may fail
110 machine.add_transition(DEPLOYING, DEPLOYFAIL, 'fail')
111
112+# A deployment may be terminated
113+machine.add_transition(DEPLOYING, DELETING, 'delete')
114+
115 # A failed deployment may be retried
116 # ironic/conductor/manager.py:do_node_deploy()
117 machine.add_transition(DEPLOYFAIL, DEPLOYING, 'rebuild')
118diff --git a/ironic/common/swift.py b/ironic/common/swift.py
119index 8fa2d65..14d6b55 100644
120--- a/ironic/common/swift.py
121+++ b/ironic/common/swift.py
122@@ -24,6 +24,7 @@ from swiftclient import utils as swift_utils
123 from ironic.common import exception
124 from ironic.common.i18n import _
125 from ironic.common import keystone
126+from ironic.common import utils
127
128 swift_opts = [
129 cfg.IntOpt('swift_max_retries',
130@@ -36,6 +37,13 @@ swift_opts = [
131 CONF = cfg.CONF
132 CONF.register_opts(swift_opts, group='swift')
133
134+CONF.import_opt('swift_endpoint_url',
135+ 'ironic.common.glance_service.v2.image_service',
136+ group='glance')
137+CONF.import_opt('swift_api_version',
138+ 'ironic.common.glance_service.v2.image_service',
139+ group='glance')
140+
141 CONF.import_opt('admin_user', 'keystonemiddleware.auth_token',
142 group='keystone_authtoken')
143 CONF.import_opt('admin_tenant_name', 'keystonemiddleware.auth_token',
144@@ -62,7 +70,9 @@ class SwiftAPI(object):
145 tenant_name=CONF.keystone_authtoken.admin_tenant_name,
146 key=CONF.keystone_authtoken.admin_password,
147 auth_url=CONF.keystone_authtoken.auth_uri,
148- auth_version=CONF.keystone_authtoken.auth_version):
149+ auth_version=CONF.keystone_authtoken.auth_version,
150+ preauthtoken=None,
151+ preauthtenant=None):
152 """Constructor for creating a SwiftAPI object.
153
154 :param user: the name of the user for Swift account
155@@ -70,16 +80,41 @@ class SwiftAPI(object):
156 :param key: the 'password' or key to authenticate with
157 :param auth_url: the url for authentication
158 :param auth_version: the version of api to use for authentication
159+ :param preauthtoken: authentication token (if you have already
160+ authenticated) note authurl/user/key/tenant_name
161+ are not required when specifying preauthtoken
162+ :param preauthtenant a tenant that will be accessed using the
163+ preauthtoken
164 """
165- auth_url = keystone.get_keystone_url(auth_url, auth_version)
166- params = {'retries': CONF.swift.swift_max_retries,
167- 'insecure': CONF.keystone_authtoken.insecure,
168- 'cacert': CONF.keystone_authtoken.cafile,
169- 'user': user,
170- 'tenant_name': tenant_name,
171- 'key': key,
172- 'authurl': auth_url,
173- 'auth_version': auth_version}
174+ params = {
175+ 'retries': CONF.swift.swift_max_retries,
176+ 'insecure': CONF.keystone_authtoken.insecure,
177+ 'cacert': CONF.keystone_authtoken.cafile
178+ }
179+
180+ if preauthtoken:
181+ # Determining swift url for the user's tenant account.
182+ tenant_id = utils.get_tenant_id(tenant_name=preauthtenant)
183+ url = "{endpoint}/{api_ver}/AUTH_{tenant}".format(
184+ endpoint=CONF.glance.swift_endpoint_url,
185+ api_ver=CONF.glance.swift_api_version,
186+ tenant=tenant_id
187+ )
188+ # authurl/user/key/tenant_name are not required when specifying
189+ # preauthtoken
190+ params.update({
191+ 'preauthtoken': preauthtoken,
192+ 'preauthurl': url
193+ })
194+ else:
195+ auth_url = keystone.get_keystone_url(auth_url, auth_version)
196+ params.update({
197+ 'user': user,
198+ 'tenant_name': tenant_name,
199+ 'key': key,
200+ 'authurl': auth_url,
201+ 'auth_version': auth_version
202+ })
203
204 self.connection = swift_client.Connection(**params)
205
206@@ -131,8 +166,8 @@ class SwiftAPI(object):
207 operation = _("head account")
208 raise exception.SwiftOperationError(operation=operation,
209 error=e)
210-
211- storage_url, token = self.connection.get_auth()
212+ storage_url = (self.connection.os_options.get('object_storage_url') or
213+ self.connection.get_auth()[0])
214 parse_result = parse.urlparse(storage_url)
215 swift_object_path = '/'.join((parse_result.path, container, object))
216 temp_url_key = account_info['x-account-meta-temp-url-key']
217@@ -189,3 +224,23 @@ class SwiftAPI(object):
218 except swift_exceptions.ClientException as e:
219 operation = _("post object")
220 raise exception.SwiftOperationError(operation=operation, error=e)
221+
222+ def get_object(self, container, object, object_headers=None,
223+ chunk_size=None):
224+ """Get Swift object.
225+
226+ :param container: The name of the container in which Swift object
227+ is placed.
228+ :param object: The name of the object in Swift
229+ :param object_headers: the headers for the object to pass to Swift
230+ :param chunk_size: size of the chunk used read to read from response
231+ :returns: Tuple (body, headers)
232+ :raises: SwiftOperationError, if operation with Swift fails.
233+ """
234+ try:
235+ return self.connection.get_object(container, object,
236+ headers=object_headers,
237+ resp_chunk_size=chunk_size)
238+ except swift_exceptions.ClientException as e:
239+ operation = _("get object")
240+ raise exception.SwiftOperationError(operation=operation, error=e)
241diff --git a/ironic/common/utils.py b/ironic/common/utils.py
242index f863087..ed4398f 100644
243--- a/ironic/common/utils.py
244+++ b/ironic/common/utils.py
245@@ -42,6 +42,7 @@ from ironic.common import exception
246 from ironic.common.i18n import _
247 from ironic.common.i18n import _LE
248 from ironic.common.i18n import _LW
249+from ironic.common import keystone
250
251 utils_opts = [
252 cfg.StrOpt('rootwrap_config',
253@@ -560,6 +561,11 @@ def is_http_url(url):
254 return url.startswith('http://') or url.startswith('https://')
255
256
257+def get_tenant_id(tenant_name):
258+ ksclient = keystone._get_ksclient()
259+ return ksclient.tenants.find(name=tenant_name).to_dict()['id']
260+
261+
262 def check_dir(directory_to_check=None, required_space=1):
263 """Check a directory is usable.
264
265diff --git a/ironic/conductor/manager.py b/ironic/conductor/manager.py
266index b4bee31..e5bb190 100644
267--- a/ironic/conductor/manager.py
268+++ b/ironic/conductor/manager.py
269@@ -768,6 +768,10 @@ class ConductorManager(periodic_task.PeriodicTasks):
270 """
271 LOG.debug("RPC do_node_tear_down called for node %s." % node_id)
272
273+ with task_manager.acquire(context, node_id, shared=True) as task:
274+ if (task.node.provision_state == states.DEPLOYING and
275+ task.driver.deploy.can_terminate_deployment):
276+ task.driver.deploy.terminate_deployment(task)
277 with task_manager.acquire(context, node_id, shared=False,
278 purpose='node tear down') as task:
279 try:
280diff --git a/ironic/drivers/base.py b/ironic/drivers/base.py
281index 098b7a0..6ebe05d 100644
282--- a/ironic/drivers/base.py
283+++ b/ironic/drivers/base.py
284@@ -345,6 +345,13 @@ class DeployInterface(BaseInterface):
285 """
286 pass
287
288+ def terminate_deployment(self, *args, **kwargs):
289+ pass
290+
291+ @property
292+ def can_terminate_deployment(self):
293+ return False
294+
295
296 @six.add_metaclass(abc.ABCMeta)
297 class BootInterface(object):
298diff --git a/ironic/drivers/modules/image_cache.py b/ironic/drivers/modules/image_cache.py
299index 8bb1e23..8e1a921 100644
300--- a/ironic/drivers/modules/image_cache.py
301+++ b/ironic/drivers/modules/image_cache.py
302@@ -27,6 +27,7 @@ from oslo_concurrency import lockutils
303 from oslo_config import cfg
304 from oslo_log import log as logging
305 from oslo_utils import fileutils
306+from oslo_utils import uuidutils
307 import six
308
309 from ironic.common import exception
310@@ -60,7 +61,8 @@ _cache_cleanup_list = []
311 class ImageCache(object):
312 """Class handling access to cache for master images."""
313
314- def __init__(self, master_dir, cache_size, cache_ttl):
315+ def __init__(self, master_dir, cache_size, cache_ttl,
316+ image_service=None):
317 """Constructor.
318
319 :param master_dir: cache directory to work on
320@@ -70,6 +72,7 @@ class ImageCache(object):
321 self.master_dir = master_dir
322 self._cache_size = cache_size
323 self._cache_ttl = cache_ttl
324+ self._image_service = image_service
325 if master_dir is not None:
326 fileutils.ensure_tree(master_dir)
327
328@@ -94,23 +97,28 @@ class ImageCache(object):
329 # NOTE(ghe): We don't share images between instances/hosts
330 if not CONF.parallel_image_downloads:
331 with lockutils.lock(img_download_lock_name, 'ironic-'):
332- _fetch(ctx, href, dest_path, force_raw)
333+ _fetch(ctx, href, dest_path,
334+ image_service=self._image_service,
335+ force_raw=force_raw)
336 else:
337- _fetch(ctx, href, dest_path, force_raw)
338+ _fetch(ctx, href, dest_path, image_service=self._image_service,
339+ force_raw=force_raw)
340 return
341
342 # TODO(ghe): have hard links and counts the same behaviour in all fs
343
344- # NOTE(vdrok): File name is converted to UUID if it's not UUID already,
345- # so that two images with same file names do not collide
346- if service_utils.is_glance_image(href):
347- master_file_name = service_utils.parse_image_ref(href)[0]
348+ if uuidutils.is_uuid_like(href):
349+ master_file_name = href
350+
351+ elif (self._image_service and
352+ hasattr(self._image_service, 'get_image_unique_id')):
353+ master_file_name = self._image_service.get_image_unique_id(href)
354+
355 else:
356- # NOTE(vdrok): Doing conversion of href in case it's unicode
357- # string, UUID cannot be generated for unicode strings on python 2.
358 href_encoded = href.encode('utf-8') if six.PY2 else href
359 master_file_name = str(uuid.uuid5(uuid.NAMESPACE_URL,
360 href_encoded))
361+
362 master_path = os.path.join(self.master_dir, master_file_name)
363
364 if CONF.parallel_image_downloads:
365@@ -121,8 +129,8 @@ class ImageCache(object):
366 # NOTE(vdrok): After rebuild requested image can change, so we
367 # should ensure that dest_path and master_path (if exists) are
368 # pointing to the same file and their content is up to date
369- cache_up_to_date = _delete_master_path_if_stale(master_path, href,
370- ctx)
371+ cache_up_to_date = _delete_master_path_if_stale(
372+ master_path, href, ctx, img_service=self._image_service)
373 dest_up_to_date = _delete_dest_path_if_stale(master_path,
374 dest_path)
375
376@@ -168,7 +176,8 @@ class ImageCache(object):
377 tmp_path = os.path.join(tmp_dir, href.split('/')[-1])
378
379 try:
380- _fetch(ctx, href, tmp_path, force_raw)
381+ _fetch(ctx, href, tmp_path, force_raw,
382+ image_service=self._image_service)
383 # NOTE(dtantsur): no need for global lock here - master_path
384 # will have link count >1 at any moment, so won't be cleaned up
385 os.link(tmp_path, master_path)
386@@ -308,10 +317,11 @@ def _free_disk_space_for(path):
387 return stat.f_frsize * stat.f_bavail
388
389
390-def _fetch(context, image_href, path, force_raw=False):
391+def _fetch(context, image_href, path, force_raw=False, image_service=None):
392 """Fetch image and convert to raw format if needed."""
393 path_tmp = "%s.part" % path
394- images.fetch(context, image_href, path_tmp, force_raw=False)
395+ images.fetch(context, image_href, path_tmp, force_raw=False,
396+ image_service=image_service)
397 # Notes(yjiang5): If glance can provide the virtual size information,
398 # then we can firstly clean cache and then invoke images.fetch().
399 if force_raw:
400@@ -384,7 +394,7 @@ def cleanup(priority):
401 return _add_property_to_class_func
402
403
404-def _delete_master_path_if_stale(master_path, href, ctx):
405+def _delete_master_path_if_stale(master_path, href, ctx, img_service=None):
406 """Delete image from cache if it is not up to date with href contents.
407
408 :param master_path: path to an image in master cache
409@@ -397,7 +407,8 @@ def _delete_master_path_if_stale(master_path, href, ctx):
410 # Glance image contents cannot be updated without changing image's UUID
411 return os.path.exists(master_path)
412 if os.path.exists(master_path):
413- img_service = image_service.get_image_service(href, context=ctx)
414+ if not img_service:
415+ img_service = image_service.get_image_service(href, context=ctx)
416 img_mtime = img_service.show(href).get('updated_at')
417 if not img_mtime:
418 # This means that href is not a glance image and doesn't have an
419diff --git a/ironic/tests/common/test_swift.py b/ironic/tests/common/test_swift.py
420index 43e3ef0..b2632c4 100644
421--- a/ironic/tests/common/test_swift.py
422+++ b/ironic/tests/common/test_swift.py
423@@ -120,6 +120,7 @@ class SwiftTestCase(base.TestCase):
424 connection_obj_mock.get_auth.return_value = auth
425 head_ret_val = {'x-account-meta-temp-url-key': 'secretkey'}
426 connection_obj_mock.head_account.return_value = head_ret_val
427+ connection_obj_mock.os_options = {}
428 gen_temp_url_mock.return_value = 'temp-url-path'
429 temp_url_returned = swiftapi.get_temp_url('container', 'object', 10)
430 connection_obj_mock.get_auth.assert_called_once_with()
431diff --git a/ironic/tests/drivers/test_image_cache.py b/ironic/tests/drivers/test_image_cache.py
432index 3d666cd..436aa49 100644
433--- a/ironic/tests/drivers/test_image_cache.py
434+++ b/ironic/tests/drivers/test_image_cache.py
435@@ -59,7 +59,7 @@ class TestImageCacheFetch(base.TestCase):
436 self.cache.fetch_image(self.uuid, self.dest_path)
437 self.assertFalse(mock_download.called)
438 mock_fetch.assert_called_once_with(
439- None, self.uuid, self.dest_path, True)
440+ None, self.uuid, self.dest_path, True, image_service=None)
441 self.assertFalse(mock_clean_up.called)
442
443 @mock.patch.object(image_cache.ImageCache, 'clean_up', autospec=True)
444@@ -75,7 +75,7 @@ class TestImageCacheFetch(base.TestCase):
445 mock_clean_up):
446 self.cache.fetch_image(self.uuid, self.dest_path)
447 mock_cache_upd.assert_called_once_with(self.master_path, self.uuid,
448- None)
449+ None, img_service=None)
450 mock_dest_upd.assert_called_once_with(self.master_path, self.dest_path)
451 self.assertFalse(mock_link.called)
452 self.assertFalse(mock_download.called)
453@@ -94,7 +94,7 @@ class TestImageCacheFetch(base.TestCase):
454 mock_clean_up):
455 self.cache.fetch_image(self.uuid, self.dest_path)
456 mock_cache_upd.assert_called_once_with(self.master_path, self.uuid,
457- None)
458+ None, img_service=None)
459 mock_dest_upd.assert_called_once_with(self.master_path, self.dest_path)
460 mock_link.assert_called_once_with(self.master_path, self.dest_path)
461 self.assertFalse(mock_download.called)
462@@ -113,7 +113,7 @@ class TestImageCacheFetch(base.TestCase):
463 mock_clean_up):
464 self.cache.fetch_image(self.uuid, self.dest_path)
465 mock_cache_upd.assert_called_once_with(self.master_path, self.uuid,
466- None)
467+ None, img_service=None)
468 mock_dest_upd.assert_called_once_with(self.master_path, self.dest_path)
469 self.assertFalse(mock_link.called)
470 mock_download.assert_called_once_with(
471@@ -134,7 +134,7 @@ class TestImageCacheFetch(base.TestCase):
472 mock_clean_up):
473 self.cache.fetch_image(self.uuid, self.dest_path)
474 mock_cache_upd.assert_called_once_with(self.master_path, self.uuid,
475- None)
476+ None, img_service=None)
477 mock_dest_upd.assert_called_once_with(self.master_path, self.dest_path)
478 self.assertFalse(mock_link.called)
479 mock_download.assert_called_once_with(
480@@ -158,7 +158,7 @@ class TestImageCacheFetch(base.TestCase):
481
482 @mock.patch.object(image_cache, '_fetch', autospec=True)
483 def test__download_image(self, mock_fetch):
484- def _fake_fetch(ctx, uuid, tmp_path, *args):
485+ def _fake_fetch(ctx, uuid, tmp_path, *args, **kwargs):
486 self.assertEqual(self.uuid, uuid)
487 self.assertNotEqual(self.dest_path, tmp_path)
488 self.assertNotEqual(os.path.dirname(tmp_path), self.master_dir)
489@@ -430,7 +430,7 @@ class TestImageCacheCleanUp(base.TestCase):
490 @mock.patch.object(utils, 'rmtree_without_raise', autospec=True)
491 @mock.patch.object(image_cache, '_fetch', autospec=True)
492 def test_temp_images_not_cleaned(self, mock_fetch, mock_rmtree):
493- def _fake_fetch(ctx, uuid, tmp_path, *args):
494+ def _fake_fetch(ctx, uuid, tmp_path, *args, **kwargs):
495 with open(tmp_path, 'w') as fp:
496 fp.write("TEST" * 10)
497
498@@ -675,7 +675,8 @@ class TestFetchCleanup(base.TestCase):
499 mock_size.return_value = 100
500 image_cache._fetch('fake', 'fake-uuid', '/foo/bar', force_raw=True)
501 mock_fetch.assert_called_once_with('fake', 'fake-uuid',
502- '/foo/bar.part', force_raw=False)
503+ '/foo/bar.part', image_service=None,
504+ force_raw=False)
505 mock_clean.assert_called_once_with('/foo', 100)
506 mock_raw.assert_called_once_with('fake-uuid', '/foo/bar',
507 '/foo/bar.part')
508
diff --git a/patches/patch-nova-stable-kilo b/patches/patch-nova-stable-liberty
index 8156970..e29466b 100644
--- a/patches/patch-nova-stable-kilo
+++ b/patches/patch-nova-stable-liberty
@@ -1,98 +1,41 @@
1diff --git a/nova/objects/image_meta.py b/nova/objects/image_meta.py
2index 15be3f1..83fc2fb 100644
3--- a/nova/objects/image_meta.py
4+++ b/nova/objects/image_meta.py
5@@ -346,6 +346,7 @@ class ImageMetaProps(base.NovaObject):
6 # is a fairly generic type. For a detailed type consider os_distro
7 # instead
8 'os_type': fields.OSTypeField(),
9+ 'deploy_config': fields.StringField(),
10 }
11
12 # The keys are the legacy property names and
13diff --git a/nova/tests/unit/objects/test_objects.py b/nova/tests/unit/objects/test_objects.py
14index 031555f..e0368b8 100644
15--- a/nova/tests/unit/objects/test_objects.py
16+++ b/nova/tests/unit/objects/test_objects.py
17@@ -1180,7 +1180,7 @@ object_data = {
18 'HostMapping': '1.0-1a3390a696792a552ab7bd31a77ba9ac',
19 'HVSpec': '1.1-6b4f7c0f688cbd03e24142a44eb9010d',
20 'ImageMeta': '1.7-642d1b2eb3e880a367f37d72dd76162d',
21- 'ImageMetaProps': '1.7-f12fc4cf3e25d616f69a66fb9d2a7aa6',
22+ 'ImageMetaProps': '1.7-716042e9e80ea16890f475200940d6f9',
23 'Instance': '2.0-ff56804dce87d81d9a04834d4bd1e3d2',
24 # NOTE(danms): Reviewers: do not approve changes to the Instance1
25 # object schema. It is frozen for Liberty and will be removed in
1diff --git a/nova/tests/unit/virt/ironic/test_driver.py b/nova/tests/unit/virt/ironic/test_driver.py 26diff --git a/nova/tests/unit/virt/ironic/test_driver.py b/nova/tests/unit/virt/ironic/test_driver.py
2index b19c6eb..6305ff7 100644 27index a8c653a..940497c 100644
3--- a/nova/tests/unit/virt/ironic/test_driver.py 28--- a/nova/tests/unit/virt/ironic/test_driver.py
4+++ b/nova/tests/unit/virt/ironic/test_driver.py 29+++ b/nova/tests/unit/virt/ironic/test_driver.py
5@@ -24,6 +24,7 @@ from oslo_utils import uuidutils 30@@ -799,6 +799,7 @@ class IronicDriverTestCase(test.NoDBTestCase):
6 from nova.api.metadata import base as instance_metadata
7 from nova.compute import power_state as nova_states
8 from nova.compute import task_states
9+from nova.compute import vm_states
10 from nova import context as nova_context
11 from nova import exception
12 from nova import objects
13@@ -143,8 +144,9 @@ class IronicDriverTestCase(test.NoDBTestCase):
14 ironic_driver._validate_instance_and_node,
15 ironicclient, instance)
16
17+ @mock.patch.object(objects.Instance, 'refresh')
18 @mock.patch.object(ironic_driver, '_validate_instance_and_node')
19- def test__wait_for_active_pass(self, fake_validate):
20+ def test__wait_for_active_pass(self, fake_validate, fake_refresh):
21 instance = fake_instance.fake_instance_obj(self.ctx,
22 uuid=uuidutils.generate_uuid())
23 node = ironic_utils.get_test_node(
24@@ -152,10 +154,12 @@ class IronicDriverTestCase(test.NoDBTestCase):
25
26 fake_validate.return_value = node
27 self.driver._wait_for_active(FAKE_CLIENT, instance)
28- self.assertTrue(fake_validate.called)
29+ fake_validate.assert_called_once_with(FAKE_CLIENT, instance)
30+ fake_refresh.assert_called_once_with()
31
32+ @mock.patch.object(objects.Instance, 'refresh')
33 @mock.patch.object(ironic_driver, '_validate_instance_and_node')
34- def test__wait_for_active_done(self, fake_validate):
35+ def test__wait_for_active_done(self, fake_validate, fake_refresh):
36 instance = fake_instance.fake_instance_obj(self.ctx,
37 uuid=uuidutils.generate_uuid())
38 node = ironic_utils.get_test_node(
39@@ -165,10 +169,12 @@ class IronicDriverTestCase(test.NoDBTestCase):
40 self.assertRaises(loopingcall.LoopingCallDone,
41 self.driver._wait_for_active,
42 FAKE_CLIENT, instance)
43- self.assertTrue(fake_validate.called)
44+ fake_validate.assert_called_once_with(FAKE_CLIENT, instance)
45+ fake_refresh.assert_called_once_with()
46
47+ @mock.patch.object(objects.Instance, 'refresh')
48 @mock.patch.object(ironic_driver, '_validate_instance_and_node')
49- def test__wait_for_active_fail(self, fake_validate):
50+ def test__wait_for_active_fail(self, fake_validate, fake_refresh):
51 instance = fake_instance.fake_instance_obj(self.ctx,
52 uuid=uuidutils.generate_uuid())
53 node = ironic_utils.get_test_node(
54@@ -178,7 +184,31 @@ class IronicDriverTestCase(test.NoDBTestCase):
55 self.assertRaises(exception.InstanceDeployFailure,
56 self.driver._wait_for_active,
57 FAKE_CLIENT, instance)
58- self.assertTrue(fake_validate.called)
59+ fake_validate.assert_called_once_with(FAKE_CLIENT, instance)
60+ fake_refresh.assert_called_once_with()
61+
62+ @mock.patch.object(objects.Instance, 'refresh')
63+ @mock.patch.object(ironic_driver, '_validate_instance_and_node')
64+ def _wait_for_active_abort(self, instance_params, fake_validate,
65+ fake_refresh):
66+ instance = fake_instance.fake_instance_obj(self.ctx,
67+ uuid=uuidutils.generate_uuid(),
68+ **instance_params)
69+ self.assertRaises(exception.InstanceDeployFailure,
70+ self.driver._wait_for_active,
71+ FAKE_CLIENT, instance)
72+ # Assert _validate_instance_and_node wasn't called
73+ self.assertFalse(fake_validate.called)
74+ fake_refresh.assert_called_once_with()
75+
76+ def test__wait_for_active_abort_deleting(self):
77+ self._wait_for_active_abort({'task_state': task_states.DELETING})
78+
79+ def test__wait_for_active_abort_deleted(self):
80+ self._wait_for_active_abort({'vm_state': vm_states.DELETED})
81+
82+ def test__wait_for_active_abort_error(self):
83+ self._wait_for_active_abort({'vm_state': vm_states.ERROR})
84
85 @mock.patch.object(ironic_driver, '_validate_instance_and_node')
86 def test__wait_for_power_state_pass(self, fake_validate):
87@@ -626,6 +656,7 @@ class IronicDriverTestCase(test.NoDBTestCase):
88 result = self.driver.macs_for_instance(instance) 31 result = self.driver.macs_for_instance(instance)
89 self.assertIsNone(result) 32 self.assertIsNone(result)
90 33
91+ @mock.patch.object(ironic_driver.IronicDriver, '_get_switch_boot_options') 34+ @mock.patch.object(ironic_driver.IronicDriver, '_get_switch_boot_options')
92 @mock.patch.object(objects.Instance, 'save') 35 @mock.patch.object(objects.Instance, 'save')
93 @mock.patch.object(loopingcall, 'FixedIntervalLoopingCall') 36 @mock.patch.object(loopingcall, 'FixedIntervalLoopingCall')
94 @mock.patch.object(FAKE_CLIENT, 'node') 37 @mock.patch.object(FAKE_CLIENT, 'node')
95@@ -634,7 +665,7 @@ class IronicDriverTestCase(test.NoDBTestCase): 38@@ -807,7 +808,7 @@ class IronicDriverTestCase(test.NoDBTestCase):
96 @mock.patch.object(ironic_driver.IronicDriver, '_plug_vifs') 39 @mock.patch.object(ironic_driver.IronicDriver, '_plug_vifs')
97 @mock.patch.object(ironic_driver.IronicDriver, '_start_firewall') 40 @mock.patch.object(ironic_driver.IronicDriver, '_start_firewall')
98 def _test_spawn(self, mock_sf, mock_pvifs, mock_adf, mock_wait_active, 41 def _test_spawn(self, mock_sf, mock_pvifs, mock_adf, mock_wait_active,
@@ -101,18 +44,18 @@ index b19c6eb..6305ff7 100644
101 node_uuid = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee' 44 node_uuid = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee'
102 node = ironic_utils.get_test_node(driver='fake', uuid=node_uuid) 45 node = ironic_utils.get_test_node(driver='fake', uuid=node_uuid)
103 instance = fake_instance.fake_instance_obj(self.ctx, node=node_uuid) 46 instance = fake_instance.fake_instance_obj(self.ctx, node=node_uuid)
104@@ -668,6 +699,7 @@ class IronicDriverTestCase(test.NoDBTestCase): 47@@ -845,6 +846,7 @@ class IronicDriverTestCase(test.NoDBTestCase):
105 fake_looping_call.start.assert_called_once_with( 48 fake_looping_call.start.assert_called_once_with(
106 interval=CONF.ironic.api_retry_interval) 49 interval=CONF.ironic.api_retry_interval)
107 fake_looping_call.wait.assert_called_once_with() 50 fake_looping_call.wait.assert_called_once_with()
108+ mock_sb_options.assert_called_once_with(self.ctx, instance, node_uuid) 51+ mock_sb_options.assert_called_once_with(self.ctx, instance, node_uuid)
109 52
110 @mock.patch.object(ironic_driver.IronicDriver, '_generate_configdrive') 53 @mock.patch.object(ironic_driver.IronicDriver, '_generate_configdrive')
111 @mock.patch.object(configdrive, 'required_by') 54 @mock.patch.object(configdrive, 'required_by')
112@@ -720,14 +752,61 @@ class IronicDriverTestCase(test.NoDBTestCase): 55@@ -897,14 +899,62 @@ class IronicDriverTestCase(test.NoDBTestCase):
113 self.driver.spawn, self.ctx, instance, None, [], None) 56 self.driver.spawn, self.ctx, instance, None, [], None)
114 mock_destroy.assert_called_once_with(self.ctx, instance, None) 57 mock_destroy.assert_called_once_with(self.ctx, instance, None)
115 58
116+ @mock.patch.object(FAKE_CLIENT, 'node') 59+ @mock.patch.object(FAKE_CLIENT, 'node')
117+ def test__get_switch_boot_options(self, mock_node): 60+ def test__get_switch_boot_options(self, mock_node):
118+ node_uuid = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee' 61+ node_uuid = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee'
@@ -160,23 +103,25 @@ index b19c6eb..6305ff7 100644
160 node = ironic_utils.get_test_node(driver='fake') 103 node = ironic_utils.get_test_node(driver='fake')
161- instance = fake_instance.fake_instance_obj(self.ctx, 104- instance = fake_instance.fake_instance_obj(self.ctx,
162- node=node.uuid) 105- node=node.uuid)
106- image_meta = ironic_utils.get_test_image_meta_object()
163+ instance = fake_instance.fake_instance_obj( 107+ instance = fake_instance.fake_instance_obj(
164+ self.ctx, 108+ self.ctx,
165+ node=node.uuid, 109+ node=node.uuid,
166+ expected_attrs=('metadata',)) 110+ expected_attrs=('metadata',))
167 image_meta = ironic_utils.get_test_image_meta()
168+ mock_get_depl_conf_opts.return_value = {'foo': 'bar123'}
169+ instance['metadata']['driver_actions'] = {'bar': 'foo123'}
170 flavor = ironic_utils.get_test_flavor() 111 flavor = ironic_utils.get_test_flavor()
171+ 112+
113+ image_meta = ironic_utils.get_test_image_meta_object()
114+ mock_get_depl_conf_opts.return_value = {'foo': 'bar123'}
115+ instance['metadata']['driver_actions'] = 'test_driver_actions'
116+
172 self.driver._add_driver_fields(node, instance, image_meta, flavor) 117 self.driver._add_driver_fields(node, instance, image_meta, flavor)
173+ 118+
174 expected_patch = [{'path': '/instance_info/image_source', 'op': 'add', 119 expected_patch = [{'path': '/instance_info/image_source', 'op': 'add',
175 'value': image_meta['id']}, 120 'value': image_meta.id},
176 {'path': '/instance_info/root_gb', 'op': 'add', 121 {'path': '/instance_info/root_gb', 'op': 'add',
177@@ -735,21 +814,96 @@ class IronicDriverTestCase(test.NoDBTestCase): 122@@ -920,21 +970,96 @@ class IronicDriverTestCase(test.NoDBTestCase):
178 {'path': '/instance_info/swap_mb', 'op': 'add', 123 {'path': '/instance_info/local_gb', 'op': 'add',
179 'value': str(flavor['swap'])}, 124 'value': str(node.properties.get('local_gb', 0))},
180 {'path': '/instance_uuid', 'op': 'add', 125 {'path': '/instance_uuid', 'op': 'add',
181- 'value': instance.uuid}] 126- 'value': instance.uuid}]
182+ 'value': instance.uuid}, 127+ 'value': instance.uuid},
@@ -185,10 +130,10 @@ index b19c6eb..6305ff7 100644
185+ 'value': {'foo': 'bar123'}}, 130+ 'value': {'foo': 'bar123'}},
186+ {'path': '/instance_info/driver_actions', 131+ {'path': '/instance_info/driver_actions',
187+ 'op': 'add', 132+ 'op': 'add',
188+ 'value': {'bar': 'foo123'}}, 133+ 'value': 'test_driver_actions'},
189+ ] 134+ ]
190 mock_update.assert_called_once_with(node.uuid, expected_patch) 135 mock_update.assert_called_once_with(node.uuid, expected_patch)
191 136
192 @mock.patch.object(FAKE_CLIENT.node, 'update') 137 @mock.patch.object(FAKE_CLIENT.node, 'update')
193 def test__add_driver_fields_fail(self, mock_update): 138 def test__add_driver_fields_fail(self, mock_update):
194 mock_update.side_effect = ironic_exception.BadRequest() 139 mock_update.side_effect = ironic_exception.BadRequest()
@@ -199,16 +144,16 @@ index b19c6eb..6305ff7 100644
199+ self.ctx, 144+ self.ctx,
200+ node=node.uuid, 145+ node=node.uuid,
201+ expected_attrs=('metadata',)) 146+ expected_attrs=('metadata',))
202 image_meta = ironic_utils.get_test_image_meta() 147 image_meta = ironic_utils.get_test_image_meta_object()
203 flavor = ironic_utils.get_test_flavor() 148 flavor = ironic_utils.get_test_flavor()
204 self.assertRaises(exception.InstanceDeployFailure, 149 self.assertRaises(exception.InstanceDeployFailure,
205 self.driver._add_driver_fields, 150 self.driver._add_driver_fields,
206 node, instance, image_meta, flavor) 151 node, instance, image_meta, flavor)
207 152
208+ def test__get_deploy_config_options_all_present(self): 153+ def test__get_deploy_config_options_all_present(self):
209+ node = ironic_utils.get_test_node( 154+ node = ironic_utils.get_test_node(
210+ driver='fake', driver_info={'deploy_config': "node-conf"}) 155+ driver='fake', driver_info={'deploy_config': "node-conf"})
211+ image_meta = ironic_utils.get_test_image_meta( 156+ image_meta = ironic_utils.get_test_image_meta_object(
212+ deploy_config="image-conf") 157+ deploy_config="image-conf")
213+ instance = fake_instance.fake_instance_obj( 158+ instance = fake_instance.fake_instance_obj(
214+ self.ctx, node=node.uuid, expected_attrs=('metadata',), 159+ self.ctx, node=node.uuid, expected_attrs=('metadata',),
@@ -233,7 +178,7 @@ index b19c6eb..6305ff7 100644
233+ "image": "previous_image_conf", 178+ "image": "previous_image_conf",
234+ }} 179+ }}
235+ ) 180+ )
236+ image_meta = ironic_utils.get_test_image_meta( 181+ image_meta = ironic_utils.get_test_image_meta_object(
237+ deploy_config="image-conf") 182+ deploy_config="image-conf")
238+ instance = fake_instance.fake_instance_obj( 183+ instance = fake_instance.fake_instance_obj(
239+ self.ctx, node=node.uuid, expected_attrs=('metadata',)) 184+ self.ctx, node=node.uuid, expected_attrs=('metadata',))
@@ -248,7 +193,7 @@ index b19c6eb..6305ff7 100644
248+ 193+
249+ def test__get_deploy_config_options_some_present(self): 194+ def test__get_deploy_config_options_some_present(self):
250+ node = ironic_utils.get_test_node(driver='fake') 195+ node = ironic_utils.get_test_node(driver='fake')
251+ image_meta = ironic_utils.get_test_image_meta() 196+ image_meta = ironic_utils.get_test_image_meta_object()
252+ instance = fake_instance.fake_instance_obj( 197+ instance = fake_instance.fake_instance_obj(
253+ self.ctx, node=node.uuid, expected_attrs=('metadata',), 198+ self.ctx, node=node.uuid, expected_attrs=('metadata',),
254+ metadata={'deploy_config': "instance-conf"}) 199+ metadata={'deploy_config': "instance-conf"})
@@ -261,7 +206,7 @@ index b19c6eb..6305ff7 100644
261+ 206+
262+ def test__get_deploy_config_options_none_present(self): 207+ def test__get_deploy_config_options_none_present(self):
263+ node = ironic_utils.get_test_node(driver='fake') 208+ node = ironic_utils.get_test_node(driver='fake')
264+ image_meta = ironic_utils.get_test_image_meta() 209+ image_meta = ironic_utils.get_test_image_meta_object()
265+ instance = fake_instance.fake_instance_obj( 210+ instance = fake_instance.fake_instance_obj(
266+ self.ctx, node=node.uuid, expected_attrs=('metadata',)) 211+ self.ctx, node=node.uuid, expected_attrs=('metadata',))
267+ 212+
@@ -274,7 +219,7 @@ index b19c6eb..6305ff7 100644
274 @mock.patch.object(FAKE_CLIENT.node, 'update') 219 @mock.patch.object(FAKE_CLIENT.node, 'update')
275 def test__cleanup_deploy_good_with_flavor(self, mock_update): 220 def test__cleanup_deploy_good_with_flavor(self, mock_update):
276 node = ironic_utils.get_test_node(driver='fake', 221 node = ironic_utils.get_test_node(driver='fake',
277@@ -781,8 +935,10 @@ class IronicDriverTestCase(test.NoDBTestCase): 222@@ -983,8 +1108,10 @@ class IronicDriverTestCase(test.NoDBTestCase):
278 node = ironic_utils.get_test_node(driver='fake', 223 node = ironic_utils.get_test_node(driver='fake',
279 instance_uuid=self.instance_uuid) 224 instance_uuid=self.instance_uuid)
280 flavor = ironic_utils.get_test_flavor(extra_specs={}) 225 flavor = ironic_utils.get_test_flavor(extra_specs={})
@@ -287,7 +232,7 @@ index b19c6eb..6305ff7 100644
287 instance.flavor = flavor 232 instance.flavor = flavor
288 self.assertRaises(exception.InstanceTerminationFailure, 233 self.assertRaises(exception.InstanceTerminationFailure,
289 self.driver._cleanup_deploy, 234 self.driver._cleanup_deploy,
290@@ -796,7 +952,8 @@ class IronicDriverTestCase(test.NoDBTestCase): 235@@ -998,7 +1125,8 @@ class IronicDriverTestCase(test.NoDBTestCase):
291 node_uuid = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee' 236 node_uuid = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee'
292 node = ironic_utils.get_test_node(driver='fake', uuid=node_uuid) 237 node = ironic_utils.get_test_node(driver='fake', uuid=node_uuid)
293 flavor = ironic_utils.get_test_flavor() 238 flavor = ironic_utils.get_test_flavor()
@@ -295,9 +240,9 @@ index b19c6eb..6305ff7 100644
295+ instance = fake_instance.fake_instance_obj( 240+ instance = fake_instance.fake_instance_obj(
296+ self.ctx, node=node_uuid, expected_attrs=('metadata',)) 241+ self.ctx, node=node_uuid, expected_attrs=('metadata',))
297 instance.flavor = flavor 242 instance.flavor = flavor
298 243
299 mock_node.validate.return_value = ironic_utils.get_test_validation( 244 mock_node.validate.return_value = ironic_utils.get_test_validation(
300@@ -821,7 +978,8 @@ class IronicDriverTestCase(test.NoDBTestCase): 245@@ -1023,7 +1151,8 @@ class IronicDriverTestCase(test.NoDBTestCase):
301 node_uuid = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee' 246 node_uuid = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee'
302 node = ironic_utils.get_test_node(driver='fake', uuid=node_uuid) 247 node = ironic_utils.get_test_node(driver='fake', uuid=node_uuid)
303 flavor = ironic_utils.get_test_flavor() 248 flavor = ironic_utils.get_test_flavor()
@@ -307,7 +252,7 @@ index b19c6eb..6305ff7 100644
307 instance.flavor = flavor 252 instance.flavor = flavor
308 mock_node.get.return_value = node 253 mock_node.get.return_value = node
309 mock_node.validate.return_value = ironic_utils.get_test_validation() 254 mock_node.validate.return_value = ironic_utils.get_test_validation()
310@@ -851,7 +1009,8 @@ class IronicDriverTestCase(test.NoDBTestCase): 255@@ -1053,7 +1182,8 @@ class IronicDriverTestCase(test.NoDBTestCase):
311 node_uuid = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee' 256 node_uuid = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee'
312 node = ironic_utils.get_test_node(driver='fake', uuid=node_uuid) 257 node = ironic_utils.get_test_node(driver='fake', uuid=node_uuid)
313 flavor = ironic_utils.get_test_flavor() 258 flavor = ironic_utils.get_test_flavor()
@@ -316,8 +261,8 @@ index b19c6eb..6305ff7 100644
316+ self.ctx, node=node_uuid, expected_attrs=('metadata',)) 261+ self.ctx, node=node_uuid, expected_attrs=('metadata',))
317 instance.flavor = flavor 262 instance.flavor = flavor
318 image_meta = ironic_utils.get_test_image_meta() 263 image_meta = ironic_utils.get_test_image_meta()
319 264
320@@ -880,7 +1039,8 @@ class IronicDriverTestCase(test.NoDBTestCase): 265@@ -1082,7 +1212,8 @@ class IronicDriverTestCase(test.NoDBTestCase):
321 node_uuid = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee' 266 node_uuid = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee'
322 node = ironic_utils.get_test_node(driver='fake', uuid=node_uuid) 267 node = ironic_utils.get_test_node(driver='fake', uuid=node_uuid)
323 flavor = ironic_utils.get_test_flavor() 268 flavor = ironic_utils.get_test_flavor()
@@ -326,9 +271,9 @@ index b19c6eb..6305ff7 100644
326+ self.ctx, node=node_uuid, expected_attrs=('metadata',)) 271+ self.ctx, node=node_uuid, expected_attrs=('metadata',))
327 instance.flavor = flavor 272 instance.flavor = flavor
328 image_meta = ironic_utils.get_test_image_meta() 273 image_meta = ironic_utils.get_test_image_meta()
329 274
330@@ -912,7 +1072,8 @@ class IronicDriverTestCase(test.NoDBTestCase): 275@@ -1113,7 +1244,8 @@ class IronicDriverTestCase(test.NoDBTestCase):
331 fake_net_info = utils.get_test_network_info() 276 node_uuid = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee'
332 node = ironic_utils.get_test_node(driver='fake', uuid=node_uuid) 277 node = ironic_utils.get_test_node(driver='fake', uuid=node_uuid)
333 flavor = ironic_utils.get_test_flavor() 278 flavor = ironic_utils.get_test_flavor()
334- instance = fake_instance.fake_instance_obj(self.ctx, node=node_uuid) 279- instance = fake_instance.fake_instance_obj(self.ctx, node=node_uuid)
@@ -336,8 +281,8 @@ index b19c6eb..6305ff7 100644
336+ self.ctx, node=node_uuid, expected_attrs=('metadata',)) 281+ self.ctx, node=node_uuid, expected_attrs=('metadata',))
337 instance.flavor = flavor 282 instance.flavor = flavor
338 image_meta = ironic_utils.get_test_image_meta() 283 image_meta = ironic_utils.get_test_image_meta()
339 284
340@@ -945,7 +1106,8 @@ class IronicDriverTestCase(test.NoDBTestCase): 285@@ -1146,7 +1278,8 @@ class IronicDriverTestCase(test.NoDBTestCase):
341 node_uuid = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee' 286 node_uuid = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee'
342 node = ironic_utils.get_test_node(driver='fake', uuid=node_uuid) 287 node = ironic_utils.get_test_node(driver='fake', uuid=node_uuid)
343 flavor = ironic_utils.get_test_flavor(ephemeral_gb=1) 288 flavor = ironic_utils.get_test_flavor(ephemeral_gb=1)
@@ -347,71 +292,17 @@ index b19c6eb..6305ff7 100644
347 instance.flavor = flavor 292 instance.flavor = flavor
348 mock_node.get_by_instance_uuid.return_value = node 293 mock_node.get_by_instance_uuid.return_value = node
349 mock_node.set_provision_state.return_value = mock.MagicMock() 294 mock_node.set_provision_state.return_value = mock.MagicMock()
350@@ -957,12 +1119,12 @@ class IronicDriverTestCase(test.NoDBTestCase): 295@@ -1541,15 +1674,16 @@ class IronicDriverTestCase(test.NoDBTestCase):
351
352 @mock.patch.object(FAKE_CLIENT, 'node')
353 @mock.patch.object(ironic_driver.IronicDriver, '_cleanup_deploy')
354- def test_destroy(self, mock_cleanup_deploy, mock_node):
355+ def _test_destroy(self, state, mock_cleanup_deploy, mock_node):
356 node_uuid = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee'
357 network_info = 'foo'
358
359 node = ironic_utils.get_test_node(driver='fake', uuid=node_uuid,
360- provision_state=ironic_states.ACTIVE)
361+ provision_state=state)
362 instance = fake_instance.fake_instance_obj(self.ctx, node=node_uuid)
363
364 def fake_set_provision_state(*_):
365@@ -971,29 +1133,22 @@ class IronicDriverTestCase(test.NoDBTestCase):
366 mock_node.get_by_instance_uuid.return_value = node
367 mock_node.set_provision_state.side_effect = fake_set_provision_state
368 self.driver.destroy(self.ctx, instance, network_info, None)
369- mock_node.set_provision_state.assert_called_once_with(node_uuid,
370- 'deleted')
371+
372 mock_node.get_by_instance_uuid.assert_called_with(instance.uuid)
373 mock_cleanup_deploy.assert_called_with(self.ctx, node,
374 instance, network_info)
375
376- @mock.patch.object(FAKE_CLIENT, 'node')
377- @mock.patch.object(ironic_driver.IronicDriver, '_cleanup_deploy')
378- def test_destroy_ignore_unexpected_state(self, mock_cleanup_deploy,
379- mock_node):
380- node_uuid = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee'
381- network_info = 'foo'
382+ # For states that makes sense check if set_provision_state has
383+ # been called
384+ if state in ironic_driver._UNPROVISION_STATES:
385+ mock_node.set_provision_state.assert_called_once_with(
386+ node_uuid, 'deleted')
387+ else:
388+ self.assertFalse(mock_node.set_provision_state.called)
389
390- node = ironic_utils.get_test_node(driver='fake', uuid=node_uuid,
391- provision_state=ironic_states.DELETING)
392- instance = fake_instance.fake_instance_obj(self.ctx, node=node_uuid)
393-
394- mock_node.get_by_instance_uuid.return_value = node
395- self.driver.destroy(self.ctx, instance, network_info, None)
396- self.assertFalse(mock_node.set_provision_state.called)
397- mock_node.get_by_instance_uuid.assert_called_with(instance.uuid)
398- mock_cleanup_deploy.assert_called_with(self.ctx, node, instance,
399- network_info)
400+ def test_destroy(self):
401+ for state in ironic_states.PROVISION_STATE_LIST:
402+ self._test_destroy(state)
403
404 @mock.patch.object(FAKE_CLIENT, 'node')
405 @mock.patch.object(ironic_driver.IronicDriver, '_cleanup_deploy')
406@@ -1287,6 +1442,7 @@ class IronicDriverTestCase(test.NoDBTestCase):
407 self.driver.refresh_instance_security_rules(fake_group) 296 self.driver.refresh_instance_security_rules(fake_group)
408 mock_risr.assert_called_once_with(fake_group) 297 mock_risr.assert_called_once_with(fake_group)
409 298
410+ @mock.patch.object(ironic_driver.IronicDriver, '_get_switch_boot_options') 299+ @mock.patch.object(ironic_driver.IronicDriver, '_get_switch_boot_options')
411 @mock.patch.object(ironic_driver.IronicDriver, '_wait_for_active') 300 @mock.patch.object(ironic_driver.IronicDriver, '_wait_for_active')
412 @mock.patch.object(loopingcall, 'FixedIntervalLoopingCall') 301 @mock.patch.object(loopingcall, 'FixedIntervalLoopingCall')
413 @mock.patch.object(FAKE_CLIENT.node, 'set_provision_state') 302 @mock.patch.object(FAKE_CLIENT.node, 'set_provision_state')
414@@ -1295,7 +1451,7 @@ class IronicDriverTestCase(test.NoDBTestCase): 303 @mock.patch.object(ironic_driver.IronicDriver, '_add_driver_fields')
304- @mock.patch.object(FAKE_CLIENT.node, 'get')
305+ @mock.patch.object(FAKE_CLIENT.node, 'get_by_instance_uuid')
415 @mock.patch.object(objects.Instance, 'save') 306 @mock.patch.object(objects.Instance, 'save')
416 def _test_rebuild(self, mock_save, mock_get, mock_driver_fields, 307 def _test_rebuild(self, mock_save, mock_get, mock_driver_fields,
417 mock_set_pstate, mock_looping, mock_wait_active, 308 mock_set_pstate, mock_looping, mock_wait_active,
@@ -420,10 +311,10 @@ index b19c6eb..6305ff7 100644
420 node_uuid = uuidutils.generate_uuid() 311 node_uuid = uuidutils.generate_uuid()
421 node = ironic_utils.get_test_node(uuid=node_uuid, 312 node = ironic_utils.get_test_node(uuid=node_uuid,
422 instance_uuid=self.instance_uuid, 313 instance_uuid=self.instance_uuid,
423@@ -1306,10 +1462,12 @@ class IronicDriverTestCase(test.NoDBTestCase): 314@@ -1560,10 +1694,12 @@ class IronicDriverTestCase(test.NoDBTestCase):
424 flavor_id = 5 315 flavor_id = 5
425 flavor = objects.Flavor(flavor_id=flavor_id, name='baremetal') 316 flavor = objects.Flavor(flavor_id=flavor_id, name='baremetal')
426 317
427- instance = fake_instance.fake_instance_obj(self.ctx, 318- instance = fake_instance.fake_instance_obj(self.ctx,
428- uuid=self.instance_uuid, 319- uuid=self.instance_uuid,
429- node=node_uuid, 320- node=node_uuid,
@@ -435,20 +326,29 @@ index b19c6eb..6305ff7 100644
435+ instance_type_id=flavor_id, 326+ instance_type_id=flavor_id,
436+ expected_attrs=('metadata',)) 327+ expected_attrs=('metadata',))
437 instance.flavor = flavor 328 instance.flavor = flavor
438 329
439 fake_looping_call = FakeLoopingCall() 330 fake_looping_call = FakeLoopingCall()
440@@ -1333,6 +1491,7 @@ class IronicDriverTestCase(test.NoDBTestCase): 331@@ -1589,6 +1725,7 @@ class IronicDriverTestCase(test.NoDBTestCase):
441 fake_looping_call.start.assert_called_once_with( 332 fake_looping_call.start.assert_called_once_with(
442 interval=CONF.ironic.api_retry_interval) 333 interval=CONF.ironic.api_retry_interval)
443 fake_looping_call.wait.assert_called_once_with() 334 fake_looping_call.wait.assert_called_once_with()
444+ mock_sb_options.assert_called_once_with(self.ctx, instance, node_uuid) 335+ mock_sb_options.assert_called_once_with(self.ctx, instance, node_uuid)
445 336
446 def test_rebuild_preserve_ephemeral(self): 337 def test_rebuild_preserve_ephemeral(self):
447 self._test_rebuild(preserve=True) 338 self._test_rebuild(preserve=True)
448@@ -1356,10 +1515,12 @@ class IronicDriverTestCase(test.NoDBTestCase): 339@@ -1598,7 +1735,7 @@ class IronicDriverTestCase(test.NoDBTestCase):
340
341 @mock.patch.object(FAKE_CLIENT.node, 'set_provision_state')
342 @mock.patch.object(ironic_driver.IronicDriver, '_add_driver_fields')
343- @mock.patch.object(FAKE_CLIENT.node, 'get')
344+ @mock.patch.object(FAKE_CLIENT.node, 'get_by_instance_uuid')
345 @mock.patch.object(objects.Instance, 'save')
346 def test_rebuild_failures(self, mock_save, mock_get, mock_driver_fields,
347 mock_set_pstate):
348@@ -1612,10 +1749,12 @@ class IronicDriverTestCase(test.NoDBTestCase):
449 flavor_id = 5 349 flavor_id = 5
450 flavor = objects.Flavor(flavor_id=flavor_id, name='baremetal') 350 flavor = objects.Flavor(flavor_id=flavor_id, name='baremetal')
451 351
452- instance = fake_instance.fake_instance_obj(self.ctx, 352- instance = fake_instance.fake_instance_obj(self.ctx,
453- uuid=self.instance_uuid, 353- uuid=self.instance_uuid,
454- node=node_uuid, 354- node=node_uuid,
@@ -460,19 +360,19 @@ index b19c6eb..6305ff7 100644
460+ instance_type_id=flavor_id, 360+ instance_type_id=flavor_id,
461+ expected_attrs=('metadata',)) 361+ expected_attrs=('metadata',))
462 instance.flavor = flavor 362 instance.flavor = flavor
463 363
464 exceptions = [ 364 exceptions = [
465@@ -1375,6 +1536,305 @@ class IronicDriverTestCase(test.NoDBTestCase): 365@@ -1631,6 +1770,316 @@ class IronicDriverTestCase(test.NoDBTestCase):
466 injected_files=None, admin_password=None, bdms=None, 366 injected_files=None, admin_password=None, bdms=None,
467 detach_block_devices=None, attach_block_devices=None) 367 detach_block_devices=None, attach_block_devices=None)
468 368
469+ @mock.patch.object(ironic_driver.IronicDriver, '_do_rebuild') 369+ @mock.patch.object(ironic_driver.IronicDriver, '_do_rebuild')
470+ @mock.patch.object(ironic_driver.IronicDriver, '_get_switch_boot_options') 370+ @mock.patch.object(ironic_driver.IronicDriver, '_get_switch_boot_options')
471+ @mock.patch.object(ironic_driver.IronicDriver, '_wait_for_active') 371+ @mock.patch.object(ironic_driver.IronicDriver, '_wait_for_active')
472+ @mock.patch.object(loopingcall, 'FixedIntervalLoopingCall') 372+ @mock.patch.object(loopingcall, 'FixedIntervalLoopingCall')
473+ @mock.patch.object(FAKE_CLIENT.node, 'set_provision_state') 373+ @mock.patch.object(FAKE_CLIENT.node, 'set_provision_state')
474+ @mock.patch.object(ironic_driver.IronicDriver, '_add_driver_fields') 374+ @mock.patch.object(ironic_driver.IronicDriver, '_add_driver_fields')
475+ @mock.patch.object(FAKE_CLIENT.node, 'get') 375+ @mock.patch.object(FAKE_CLIENT.node, 'get_by_instance_uuid')
476+ @mock.patch.object(objects.Instance, 'save') 376+ @mock.patch.object(objects.Instance, 'save')
477+ def test_rebuild_multiboot_force_rebuild(self, mock_save, mock_get, 377+ def test_rebuild_multiboot_force_rebuild(self, mock_save, mock_get,
478+ mock_driver_fields, 378+ mock_driver_fields,
@@ -486,7 +386,8 @@ index b19c6eb..6305ff7 100644
486+ instance_info={'multiboot': True}) 386+ instance_info={'multiboot': True})
487+ mock_get.return_value = node 387+ mock_get.return_value = node
488+ 388+
489+ image_meta = ironic_utils.get_test_image_meta() 389+ image_meta = ironic_utils.get_test_image_meta_object()
390+
490+ flavor_id = 5 391+ flavor_id = 5
491+ flavor = objects.Flavor(flavor_id=flavor_id, name='baremetal') 392+ flavor = objects.Flavor(flavor_id=flavor_id, name='baremetal')
492+ 393+
@@ -502,11 +403,14 @@ index b19c6eb..6305ff7 100644
502+ fake_looping_call = FakeLoopingCall() 403+ fake_looping_call = FakeLoopingCall()
503+ mock_looping.return_value = fake_looping_call 404+ mock_looping.return_value = fake_looping_call
504+ 405+
505+ self.driver.rebuild( 406+ with mock.patch.object(objects.ImageMeta,
506+ context=self.ctx, instance=instance, image_meta=image_meta, 407+ 'from_dict') as mock_image_meta_from_dict:
507+ injected_files=None, admin_password=None, bdms=None, 408+ mock_image_meta_from_dict.return_value = image_meta
508+ detach_block_devices=None, attach_block_devices=None, 409+ self.driver.rebuild(
509+ preserve_ephemeral=False) 410+ context=self.ctx, instance=instance, image_meta=image_meta,
411+ injected_files=None, admin_password=None, bdms=None,
412+ detach_block_devices=None, attach_block_devices=None,
413+ preserve_ephemeral=False)
510+ 414+
511+ rebuild_mock.assert_called_once_with( 415+ rebuild_mock.assert_called_once_with(
512+ self.ctx, FAKE_CLIENT_WRAPPER, node, instance, image_meta, 416+ self.ctx, FAKE_CLIENT_WRAPPER, node, instance, image_meta,
@@ -517,18 +421,25 @@ index b19c6eb..6305ff7 100644
517+ block_device_info=None, 421+ block_device_info=None,
518+ preserve_ephemeral=False) 422+ preserve_ephemeral=False)
519+ 423+
424+ @mock.patch.object(objects.ImageMeta, 'from_dict')
425+ @mock.patch.object(FAKE_CLIENT.node, 'get_by_instance_uuid')
520+ @mock.patch.object(FAKE_CLIENT.node, 'get') 426+ @mock.patch.object(FAKE_CLIENT.node, 'get')
521+ @mock.patch.object(ironic_driver.IronicDriver, '_do_switch_boot_device') 427+ @mock.patch.object(ironic_driver.IronicDriver, '_do_switch_boot_device')
522+ @mock.patch.object(objects.Instance, 'save') 428+ @mock.patch.object(objects.Instance, 'save')
523+ def test_rebuild_multiboot_switch_boot(self, mock_save, 429+ def test_rebuild_multiboot_switch_boot(self, mock_save,
524+ mock_sb, mock_get): 430+ mock_sb, mock_get,
431+ mock_get_by_instance,
432+ mock_image_meta_from_dict):
525+ node_uuid = uuidutils.generate_uuid() 433+ node_uuid = uuidutils.generate_uuid()
526+ node = ironic_utils.get_test_node(uuid=node_uuid, 434+ node = ironic_utils.get_test_node(uuid=node_uuid,
527+ instance_uuid=self.instance_uuid, 435+ instance_uuid=self.instance_uuid,
528+ instance_type_id=5, 436+ instance_type_id=5,
529+ instance_info={'multiboot': True}) 437+ instance_info={'multiboot': True})
530+ mock_get.return_value = node 438+ mock_get.return_value = mock_get_by_instance.return_value = node
531+ image_meta = ironic_utils.get_test_image_meta() 439+
440+ image_meta = ironic_utils.get_test_image_meta_object()
441+ mock_image_meta_from_dict.return_value = image_meta
442+
532+ flavor_id = 5 443+ flavor_id = 5
533+ instance = fake_instance.fake_instance_obj( 444+ instance = fake_instance.fake_instance_obj(
534+ self.ctx, 445+ self.ctx,
@@ -558,7 +469,7 @@ index b19c6eb..6305ff7 100644
558+ instance_type_id=5, 469+ instance_type_id=5,
559+ instance_info={'multiboot': True}) 470+ instance_info={'multiboot': True})
560+ 471+
561+ image_meta = ironic_utils.get_test_image_meta() 472+ image_meta = ironic_utils.get_test_image_meta_object()
562+ flavor_id = 5 473+ flavor_id = 5
563+ instance = fake_instance.fake_instance_obj( 474+ instance = fake_instance.fake_instance_obj(
564+ self.ctx, 475+ self.ctx,
@@ -573,13 +484,13 @@ index b19c6eb..6305ff7 100644
573+ node, instance, image_meta) 484+ node, instance, image_meta)
574+ 485+
575+ vp_mock.assert_called_once_with(node_uuid, 'switch_boot', 486+ vp_mock.assert_called_once_with(node_uuid, 'switch_boot',
576+ {'image': image_meta['id'], 487+ {'image': image_meta.id,
577+ 'ssh_user': 'usr1', 488+ 'ssh_user': 'usr1',
578+ 'ssh_key': 'key1'}) 489+ 'ssh_key': 'key1'})
579+ sp_mock.assert_called_once_with(node_uuid, 'reboot') 490+ sp_mock.assert_called_once_with(node_uuid, 'reboot')
580+ upd_mock.assert_called_once_with( 491+ upd_mock.assert_called_once_with(
581+ node_uuid, [{'path': '/instance_info/image_source', 'op': 'add', 492+ node_uuid, [{'path': '/instance_info/image_source', 'op': 'add',
582+ 'value': image_meta['id']}]) 493+ 'value': image_meta.id}])
583+ 494+
584+ @mock.patch.object(FAKE_CLIENT.node, 'vendor_passthru') 495+ @mock.patch.object(FAKE_CLIENT.node, 'vendor_passthru')
585+ @mock.patch.object(FAKE_CLIENT.node, 'set_power_state') 496+ @mock.patch.object(FAKE_CLIENT.node, 'set_power_state')
@@ -633,7 +544,7 @@ index b19c6eb..6305ff7 100644
633+ 'image_source': 'original_image', 544+ 'image_source': 'original_image',
634+ }) 545+ })
635+ 546+
636+ image_meta = ironic_utils.get_test_image_meta() 547+ image_meta = ironic_utils.get_test_image_meta_object()
637+ flavor_id = 5 548+ flavor_id = 5
638+ instance = fake_instance.fake_instance_obj( 549+ instance = fake_instance.fake_instance_obj(
639+ self.ctx, 550+ self.ctx,
@@ -671,7 +582,7 @@ index b19c6eb..6305ff7 100644
671+ 'image_source': 'original_image', 582+ 'image_source': 'original_image',
672+ }) 583+ })
673+ 584+
674+ image_meta = ironic_utils.get_test_image_meta() 585+ image_meta = ironic_utils.get_test_image_meta_object()
675+ flavor_id = 5 586+ flavor_id = 5
676+ instance = fake_instance.fake_instance_obj( 587+ instance = fake_instance.fake_instance_obj(
677+ self.ctx, 588+ self.ctx,
@@ -707,7 +618,7 @@ index b19c6eb..6305ff7 100644
707+ 'image_source': 'original_image', 618+ 'image_source': 'original_image',
708+ }) 619+ })
709+ 620+
710+ image_meta = ironic_utils.get_test_image_meta() 621+ image_meta = ironic_utils.get_test_image_meta_object()
711+ flavor_id = 5 622+ flavor_id = 5
712+ instance = fake_instance.fake_instance_obj( 623+ instance = fake_instance.fake_instance_obj(
713+ self.ctx, 624+ self.ctx,
@@ -744,7 +655,7 @@ index b19c6eb..6305ff7 100644
744+ 'image_source': 'original_image', 655+ 'image_source': 'original_image',
745+ }) 656+ })
746+ 657+
747+ image_meta = ironic_utils.get_test_image_meta() 658+ image_meta = ironic_utils.get_test_image_meta_object()
748+ flavor_id = 5 659+ flavor_id = 5
749+ instance = fake_instance.fake_instance_obj( 660+ instance = fake_instance.fake_instance_obj(
750+ self.ctx, 661+ self.ctx,
@@ -765,24 +676,25 @@ index b19c6eb..6305ff7 100644
765+ instance.metadata['switch_boot_error']) 676+ instance.metadata['switch_boot_error'])
766+ mock_save.assert_called_once_with() 677+ mock_save.assert_called_once_with()
767+ 678+
768 679
769 @mock.patch.object(instance_metadata, 'InstanceMetadata') 680 @mock.patch.object(instance_metadata, 'InstanceMetadata')
770 @mock.patch.object(configdrive, 'ConfigDriveBuilder') 681 @mock.patch.object(configdrive, 'ConfigDriveBuilder')
771diff --git a/nova/tests/unit/virt/ironic/utils.py b/nova/tests/unit/virt/ironic/utils.py 682diff --git a/nova/tests/unit/virt/ironic/utils.py b/nova/tests/unit/virt/ironic/utils.py
772index d43f290..f3ec825 100644 683index 0e67919..66eede3 100644
773--- a/nova/tests/unit/virt/ironic/utils.py 684--- a/nova/tests/unit/virt/ironic/utils.py
774+++ b/nova/tests/unit/virt/ironic/utils.py 685+++ b/nova/tests/unit/virt/ironic/utils.py
775@@ -42,6 +42,7 @@ def get_test_node(**kw): 686@@ -39,7 +39,7 @@ def get_test_node(**kw):
687 ironic_states.NOSTATE),
688 'last_error': kw.get('last_error'),
689 'instance_uuid': kw.get('instance_uuid'),
690- 'instance_info': kw.get('instance_info'),
691+ 'instance_info': kw.get('instance_info', {}),
776 'driver': kw.get('driver', 'fake'), 692 'driver': kw.get('driver', 'fake'),
777 'driver_info': kw.get('driver_info', {}), 693 'driver_info': kw.get('driver_info', {}),
778 'properties': kw.get('properties', {}), 694 'properties': kw.get('properties', {}),
779+ 'instance_info': kw.get('instance_info', {}), 695@@ -91,7 +91,11 @@ def get_test_flavor(**kw):
780 'reservation': kw.get('reservation'), 696
781 'maintenance': kw.get('maintenance', False), 697
782 'extra': kw.get('extra', {}),
783@@ -72,7 +73,11 @@ def get_test_flavor(**kw):
784
785
786 def get_test_image_meta(**kw): 698 def get_test_image_meta(**kw):
787- return {'id': kw.get('id', 'cccccccc-cccc-cccc-cccc-cccccccccccc')} 699- return {'id': kw.get('id', 'cccccccc-cccc-cccc-cccc-cccccccccccc')}
788+ return {'id': kw.get('id', 'cccccccc-cccc-cccc-cccc-cccccccccccc'), 700+ return {'id': kw.get('id', 'cccccccc-cccc-cccc-cccc-cccccccccccc'),
@@ -790,43 +702,24 @@ index d43f290..f3ec825 100644
790+ 'deploy_config': kw.get('deploy_config', ''), 702+ 'deploy_config': kw.get('deploy_config', ''),
791+ 'driver_actions': kw.get('driver_actions', ''), 703+ 'driver_actions': kw.get('driver_actions', ''),
792+ }} 704+ }}
793 705
794 706
795 class FakePortClient(object): 707 def get_test_image_meta_object(**kw):
796@@ -110,6 +115,9 @@ class FakeNodeClient(object): 708@@ -134,6 +138,9 @@ class FakeNodeClient(object):
797 def validate(self, node_uuid): 709 def validate(self, node_uuid):
798 pass 710 pass
799 711
800+ def vendor_passthru(self, node_uuid, method, args): 712+ def vendor_passthru(self, node_uuid, method, args):
801+ pass 713+ pass
802+ 714+
803 715
804 class FakeClient(object): 716 class FakeClient(object):
805 717
806diff --git a/nova/virt/ironic/driver.py b/nova/virt/ironic/driver.py 718diff --git a/nova/virt/ironic/driver.py b/nova/virt/ironic/driver.py
807index b21c782..81dcdba 100644 719index 194221e..062f3d7 100644
808--- a/nova/virt/ironic/driver.py 720--- a/nova/virt/ironic/driver.py
809+++ b/nova/virt/ironic/driver.py 721+++ b/nova/virt/ironic/driver.py
810@@ -40,6 +40,7 @@ from nova.compute import hv_type 722@@ -391,6 +391,17 @@ class IronicDriver(virt_driver.ComputeDriver):
811 from nova.compute import power_state
812 from nova.compute import task_states
813 from nova.compute import vm_mode
814+from nova.compute import vm_states
815 from nova import context as nova_context
816 from nova import exception
817 from nova.i18n import _
818@@ -107,6 +108,10 @@ _POWER_STATE_MAP = {
819 ironic_states.POWER_OFF: power_state.SHUTDOWN,
820 }
821
822+_UNPROVISION_STATES = (ironic_states.ACTIVE, ironic_states.DEPLOYFAIL,
823+ ironic_states.ERROR, ironic_states.DEPLOYWAIT,
824+ ironic_states.DEPLOYING)
825+
826
827 def map_power_state(state):
828 try:
829@@ -326,6 +331,17 @@ class IronicDriver(virt_driver.ComputeDriver):
830 # Associate the node with an instance 723 # Associate the node with an instance
831 patch.append({'path': '/instance_uuid', 'op': 'add', 724 patch.append({'path': '/instance_uuid', 'op': 'add',
832 'value': instance.uuid}) 725 'value': instance.uuid})
@@ -844,35 +737,21 @@ index b21c782..81dcdba 100644
844 try: 737 try:
845 self.ironicclient.call('node.update', node.uuid, patch) 738 self.ironicclient.call('node.update', node.uuid, patch)
846 except ironic.exc.BadRequest: 739 except ironic.exc.BadRequest:
847@@ -335,6 +351,13 @@ class IronicDriver(virt_driver.ComputeDriver): 740@@ -400,6 +411,12 @@ class IronicDriver(virt_driver.ComputeDriver):
848 LOG.error(msg) 741 LOG.error(msg)
849 raise exception.InstanceDeployFailure(msg) 742 raise exception.InstanceDeployFailure(msg)
850 743
851+ def _update_driver_fields_after_switch_boot(self, context, node, 744+ def _update_driver_fields_after_switch_boot(self, context, node,
852+ instance, image_meta): 745+ instance, image_meta):
853+ patch = [] 746+ patch = [{'path': '/instance_info/image_source', 'op': 'add',
854+ patch.append({'path': '/instance_info/image_source', 'op': 'add', 747+ 'value': image_meta.id}]
855+ 'value': image_meta.get('id')})
856+ self.ironicclient.call('node.update', node.uuid, patch) 748+ self.ironicclient.call('node.update', node.uuid, patch)
857+ 749+
858 def _cleanup_deploy(self, context, node, instance, network_info, 750 def _cleanup_deploy(self, context, node, instance, network_info,
859 flavor=None): 751 flavor=None):
860 if flavor is None: 752 if flavor is None:
861@@ -358,6 +381,12 @@ class IronicDriver(virt_driver.ComputeDriver): 753@@ -807,9 +824,19 @@ class IronicDriver(virt_driver.ComputeDriver):
862 754
863 def _wait_for_active(self, ironicclient, instance):
864 """Wait for the node to be marked as ACTIVE in Ironic."""
865+ instance.refresh()
866+ if (instance.task_state == task_states.DELETING or
867+ instance.vm_state in (vm_states.ERROR, vm_states.DELETED)):
868+ raise exception.InstanceDeployFailure(
869+ _("Instance %s provisioning was aborted") % instance.uuid)
870+
871 node = _validate_instance_and_node(ironicclient, instance)
872 if node.provision_state == ironic_states.ACTIVE:
873 # job is done
874@@ -714,9 +743,19 @@ class IronicDriver(virt_driver.ComputeDriver):
875
876 # trigger the node deploy 755 # trigger the node deploy
877 try: 756 try:
878- self.ironicclient.call("node.set_provision_state", node_uuid, 757- self.ironicclient.call("node.set_provision_state", node_uuid,
@@ -894,7 +773,7 @@ index b21c782..81dcdba 100644
894 except Exception as e: 773 except Exception as e:
895 with excutils.save_and_reraise_exception(): 774 with excutils.save_and_reraise_exception():
896 msg = (_LE("Failed to request Ironic to provision instance " 775 msg = (_LE("Failed to request Ironic to provision instance "
897@@ -739,6 +778,17 @@ class IronicDriver(virt_driver.ComputeDriver): 776@@ -834,6 +861,17 @@ class IronicDriver(virt_driver.ComputeDriver):
898 {'instance': instance.uuid, 777 {'instance': instance.uuid,
899 'node': node_uuid}) 778 'node': node_uuid})
900 self.destroy(context, instance, network_info) 779 self.destroy(context, instance, network_info)
@@ -909,27 +788,21 @@ index b21c782..81dcdba 100644
909+ available_images = [img['image_name'] for img in 788+ available_images = [img['image_name'] for img in
910+ multiboot_meta.get('elements', [])] 789+ multiboot_meta.get('elements', [])]
911+ instance.metadata['available_images'] = str(available_images) 790+ instance.metadata['available_images'] = str(available_images)
912 791
913 def _unprovision(self, ironicclient, instance, node): 792 def _unprovision(self, ironicclient, instance, node):
914 """This method is called from destroy() to unprovision 793 """This method is called from destroy() to unprovision
915@@ -814,10 +864,7 @@ class IronicDriver(virt_driver.ComputeDriver): 794@@ -1188,16 +1226,102 @@ class IronicDriver(virt_driver.ComputeDriver):
916 # without raising any exceptions. 795 instance.task_state = task_states.REBUILD_SPAWNING
917 return 796 instance.save(expected_task_state=[task_states.REBUILDING])
918 797
919- if node.provision_state in (ironic_states.ACTIVE, 798- node_uuid = instance.node
920- ironic_states.DEPLOYFAIL, 799- node = self.ironicclient.call("node.get", node_uuid)
921- ironic_states.ERROR, 800+ # NOTE(oberezovskyi): Required to get real node uuid assigned to nova
922- ironic_states.DEPLOYWAIT): 801+ # instance. Workaround after
923+ if node.provision_state in _UNPROVISION_STATES: 802+ # Change-Id: I0233f964d8f294f0ffd9edcb16b1aaf93486177f
924 self._unprovision(self.ironicclient, instance, node) 803+ node = self.ironicclient.call("node.get_by_instance_uuid",
925 804+ instance.uuid)
926 self._cleanup_deploy(context, node, instance, network_info) 805+
927@@ -1074,24 +1121,127 @@ class IronicDriver(virt_driver.ComputeDriver):
928 node_uuid = instance.node
929 node = self.ironicclient.call("node.get", node_uuid)
930
931- self._add_driver_fields(node, instance, image_meta, instance.flavor,
932- preserve_ephemeral)
933+ # NOTE(lobur): set_provision_state to 806+ # NOTE(lobur): set_provision_state to
934+ # ACTIVE, REBUILD, and switch_boot_device are the only Ironic API 807+ # ACTIVE, REBUILD, and switch_boot_device are the only Ironic API
935+ # calls where the user context needs to be passed to Ironic. This 808+ # calls where the user context needs to be passed to Ironic. This
@@ -957,28 +830,32 @@ index b21c782..81dcdba 100644
957+ recreate=recreate, 830+ recreate=recreate,
958+ block_device_info=block_device_info, 831+ block_device_info=block_device_info,
959+ preserve_ephemeral=preserve_ephemeral) 832+ preserve_ephemeral=preserve_ephemeral)
960 833
834- self._add_driver_fields(node, instance, image_meta, instance.flavor,
835- preserve_ephemeral)
836+ self._get_switch_boot_options(context, instance, node.uuid)
837
961- # Trigger the node rebuild/redeploy. 838- # Trigger the node rebuild/redeploy.
962+ self._get_switch_boot_options(context, instance, node_uuid)
963+
964+ def _do_switch_boot_device(self, context, ironicclient, node, instance, 839+ def _do_switch_boot_device(self, context, ironicclient, node, instance,
965+ image_meta): 840+ image_meta):
966+ old_image_ref = node.instance_info.get("image_source", "") 841+ old_image_ref = node.instance_info.get("image_source", "")
967+ try: 842 try:
843- self.ironicclient.call("node.set_provision_state",
844- node_uuid, ironic_states.REBUILD)
968+ sb_user, sb_key = self._get_switch_boot_user_key(instance.metadata) 845+ sb_user, sb_key = self._get_switch_boot_user_key(instance.metadata)
969+ args = dict(ssh_user=sb_user, 846+ args = dict(ssh_user=sb_user,
970+ ssh_key=sb_key, 847+ ssh_key=sb_key,
971+ image=image_meta['id']) 848+ image=image_meta.id)
972+ ironicclient.call("node.vendor_passthru", 849+ ironicclient.call("node.vendor_passthru",
973+ node.uuid, "switch_boot", 850+ node.uuid, "switch_boot",
974+ args) 851+ args)
975+ self.ironicclient.call("node.set_power_state", node.uuid, 'reboot') 852+ self.ironicclient.call("node.set_power_state", node.uuid, 'reboot')
976+ self._update_driver_fields_after_switch_boot( 853+ self._update_driver_fields_after_switch_boot(
977+ context, node, instance, image_meta) 854+ context, node, instance, image_meta)
978+ except (exception.InvalidMetadata, # Bad Nova API call 855+ except (exception.InvalidMetadata, # Bad Nova API call
979+ exception.NovaException, # Retry failed 856+ exception.NovaException, # Retry failed
980+ ironic.exc.InternalServerError, # Validations 857+ ironic.exc.InternalServerError, # Validations
981+ ironic.exc.BadRequest) as e: # Maintenance or no such API 858+ ironic.exc.BadRequest) as e: # Maintenance or no such API
982+ # Ironic Vendor API always return 200/400/500, so the only way 859+ # Ironic Vendor API always return 200/400/500, so the only way
983+ # to check the error is introspecting its message. 860+ # to check the error is introspecting its message.
984+ if "Already in desired boot device" in six.text_type(e): 861+ if "Already in desired boot device" in six.text_type(e):
@@ -1004,7 +881,7 @@ index b21c782..81dcdba 100644
1004+ else: 881+ else:
1005+ raise exception.InvalidMetadata( 882+ raise exception.InvalidMetadata(
1006+ reason="To trigger switch boot device flow, both 'sb_user' " 883+ reason="To trigger switch boot device flow, both 'sb_user' "
1007+ "and 'sb_key' metadata params are required. To " 884+ "and 'sb_key' metadata params are required. To "s
1008+ "trigger a standard rebuild flow, use " 885+ "trigger a standard rebuild flow, use "
1009+ "force_rebuild=True metadata flag.") 886+ "force_rebuild=True metadata flag.")
1010+ 887+
@@ -1017,31 +894,17 @@ index b21c782..81dcdba 100644
1017+ 894+
1018+ self._add_driver_fields(node, instance, image_meta, 895+ self._add_driver_fields(node, instance, image_meta,
1019+ instance.flavor, preserve_ephemeral) 896+ instance.flavor, preserve_ephemeral)
1020 try: 897+ try:
1021- self.ironicclient.call("node.set_provision_state",
1022- node_uuid, ironic_states.REBUILD)
1023+ 898+
1024+ ironicclient.call("node.set_provision_state", 899+ ironicclient.call("node.set_provision_state",
1025+ node.uuid, ironic_states.REBUILD) 900+ node.uuid, ironic_states.REBUILD)
1026 except (exception.NovaException, # Retry failed 901 except (exception.NovaException, # Retry failed
1027 ironic.exc.InternalServerError, # Validations 902 ironic.exc.InternalServerError, # Validations
1028 ironic.exc.BadRequest) as e: # Maintenance 903 ironic.exc.BadRequest) as e: # Maintenance
1029 msg = (_("Failed to request Ironic to rebuild instance " 904@@ -1213,3 +1337,22 @@ class IronicDriver(virt_driver.ComputeDriver):
1030- "%(inst)s: %(reason)s") % {'inst': instance.uuid,
1031- 'reason': six.text_type(e)})
1032+ "%(inst)s: %(reason)s") %
1033+ {'inst': instance.uuid,
1034+ 'reason': six.text_type(e)})
1035 raise exception.InstanceDeployFailure(msg)
1036
1037- # Although the target provision state is REBUILD, it will actually go
1038- # to ACTIVE once the redeploy is finished.
1039+ # Although the target provision state is REBUILD, it will
1040+ # actually go to ACTIVE once the redeploy is finished.
1041 timer = loopingcall.FixedIntervalLoopingCall(self._wait_for_active,
1042 self.ironicclient,
1043 instance) 905 instance)
1044 timer.start(interval=CONF.ironic.api_retry_interval).wait() 906 timer.start(interval=CONF.ironic.api_retry_interval).wait()
907 LOG.info(_LI('Instance was successfully rebuilt'), instance=instance)
1045+ 908+
1046+ def _get_deploy_config_options(self, node, instance, image_meta): 909+ def _get_deploy_config_options(self, node, instance, image_meta):
1047+ # Taking into account previous options, if any. This is to support 910+ # Taking into account previous options, if any. This is to support
@@ -1051,7 +914,7 @@ index b21c782..81dcdba 100644
1051+ res = node.instance_info.get('deploy_config_options', {}) 914+ res = node.instance_info.get('deploy_config_options', {})
1052+ 915+
1053+ curr_options = { 916+ curr_options = {
1054+ 'image': image_meta.get('properties', {}).get('deploy_config', ''), 917+ 'image': image_meta.properties.get('deploy_config', ''),
1055+ 'instance': instance.metadata.get('deploy_config', ''), 918+ 'instance': instance.metadata.get('deploy_config', ''),
1056+ 'node': node.driver_info.get('deploy_config', ''), 919+ 'node': node.driver_info.get('deploy_config', ''),
1057+ } 920+ }
@@ -1061,21 +924,3 @@ index b21c782..81dcdba 100644
1061+ # Override previous by current. 924+ # Override previous by current.
1062+ res.update(curr_options) 925+ res.update(curr_options)
1063+ return res 926+ return res
1064diff --git a/nova/virt/ironic/ironic_states.py b/nova/virt/ironic/ironic_states.py
1065index e521f16..a02ddcf 100644
1066--- a/nova/virt/ironic/ironic_states.py
1067+++ b/nova/virt/ironic/ironic_states.py
1068@@ -138,3 +138,13 @@ POWER_OFF = 'power off'
1069
1070 REBOOT = 'rebooting'
1071 """ Node is rebooting. """
1072+
1073+##################
1074+# Helper constants
1075+##################
1076+
1077+PROVISION_STATE_LIST = (NOSTATE, MANAGEABLE, AVAILABLE, ACTIVE, DEPLOYWAIT,
1078+ DEPLOYING, DEPLOYFAIL, DEPLOYDONE, DELETING, DELETED,
1079+ CLEANING, CLEANFAIL, ERROR, REBUILD,
1080+ INSPECTING, INSPECTFAIL)
1081+""" A list of all provision states. """