bareon-ironic/patches/patch-nova-stable-kilo

1082 lines
50 KiB
Plaintext

diff --git a/nova/tests/unit/virt/ironic/test_driver.py b/nova/tests/unit/virt/ironic/test_driver.py
index b19c6eb..6305ff7 100644
--- a/nova/tests/unit/virt/ironic/test_driver.py
+++ b/nova/tests/unit/virt/ironic/test_driver.py
@@ -24,6 +24,7 @@ from oslo_utils import uuidutils
from nova.api.metadata import base as instance_metadata
from nova.compute import power_state as nova_states
from nova.compute import task_states
+from nova.compute import vm_states
from nova import context as nova_context
from nova import exception
from nova import objects
@@ -143,8 +144,9 @@ class IronicDriverTestCase(test.NoDBTestCase):
ironic_driver._validate_instance_and_node,
ironicclient, instance)
+ @mock.patch.object(objects.Instance, 'refresh')
@mock.patch.object(ironic_driver, '_validate_instance_and_node')
- def test__wait_for_active_pass(self, fake_validate):
+ def test__wait_for_active_pass(self, fake_validate, fake_refresh):
instance = fake_instance.fake_instance_obj(self.ctx,
uuid=uuidutils.generate_uuid())
node = ironic_utils.get_test_node(
@@ -152,10 +154,12 @@ class IronicDriverTestCase(test.NoDBTestCase):
fake_validate.return_value = node
self.driver._wait_for_active(FAKE_CLIENT, instance)
- self.assertTrue(fake_validate.called)
+ fake_validate.assert_called_once_with(FAKE_CLIENT, instance)
+ fake_refresh.assert_called_once_with()
+ @mock.patch.object(objects.Instance, 'refresh')
@mock.patch.object(ironic_driver, '_validate_instance_and_node')
- def test__wait_for_active_done(self, fake_validate):
+ def test__wait_for_active_done(self, fake_validate, fake_refresh):
instance = fake_instance.fake_instance_obj(self.ctx,
uuid=uuidutils.generate_uuid())
node = ironic_utils.get_test_node(
@@ -165,10 +169,12 @@ class IronicDriverTestCase(test.NoDBTestCase):
self.assertRaises(loopingcall.LoopingCallDone,
self.driver._wait_for_active,
FAKE_CLIENT, instance)
- self.assertTrue(fake_validate.called)
+ fake_validate.assert_called_once_with(FAKE_CLIENT, instance)
+ fake_refresh.assert_called_once_with()
+ @mock.patch.object(objects.Instance, 'refresh')
@mock.patch.object(ironic_driver, '_validate_instance_and_node')
- def test__wait_for_active_fail(self, fake_validate):
+ def test__wait_for_active_fail(self, fake_validate, fake_refresh):
instance = fake_instance.fake_instance_obj(self.ctx,
uuid=uuidutils.generate_uuid())
node = ironic_utils.get_test_node(
@@ -178,7 +184,31 @@ class IronicDriverTestCase(test.NoDBTestCase):
self.assertRaises(exception.InstanceDeployFailure,
self.driver._wait_for_active,
FAKE_CLIENT, instance)
- self.assertTrue(fake_validate.called)
+ fake_validate.assert_called_once_with(FAKE_CLIENT, instance)
+ fake_refresh.assert_called_once_with()
+
+ @mock.patch.object(objects.Instance, 'refresh')
+ @mock.patch.object(ironic_driver, '_validate_instance_and_node')
+ def _wait_for_active_abort(self, instance_params, fake_validate,
+ fake_refresh):
+ instance = fake_instance.fake_instance_obj(self.ctx,
+ uuid=uuidutils.generate_uuid(),
+ **instance_params)
+ self.assertRaises(exception.InstanceDeployFailure,
+ self.driver._wait_for_active,
+ FAKE_CLIENT, instance)
+ # Assert _validate_instance_and_node wasn't called
+ self.assertFalse(fake_validate.called)
+ fake_refresh.assert_called_once_with()
+
+ def test__wait_for_active_abort_deleting(self):
+ self._wait_for_active_abort({'task_state': task_states.DELETING})
+
+ def test__wait_for_active_abort_deleted(self):
+ self._wait_for_active_abort({'vm_state': vm_states.DELETED})
+
+ def test__wait_for_active_abort_error(self):
+ self._wait_for_active_abort({'vm_state': vm_states.ERROR})
@mock.patch.object(ironic_driver, '_validate_instance_and_node')
def test__wait_for_power_state_pass(self, fake_validate):
@@ -626,6 +656,7 @@ class IronicDriverTestCase(test.NoDBTestCase):
result = self.driver.macs_for_instance(instance)
self.assertIsNone(result)
+ @mock.patch.object(ironic_driver.IronicDriver, '_get_switch_boot_options')
@mock.patch.object(objects.Instance, 'save')
@mock.patch.object(loopingcall, 'FixedIntervalLoopingCall')
@mock.patch.object(FAKE_CLIENT, 'node')
@@ -634,7 +665,7 @@ class IronicDriverTestCase(test.NoDBTestCase):
@mock.patch.object(ironic_driver.IronicDriver, '_plug_vifs')
@mock.patch.object(ironic_driver.IronicDriver, '_start_firewall')
def _test_spawn(self, mock_sf, mock_pvifs, mock_adf, mock_wait_active,
- mock_node, mock_looping, mock_save):
+ mock_node, mock_looping, mock_save, mock_sb_options):
node_uuid = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee'
node = ironic_utils.get_test_node(driver='fake', uuid=node_uuid)
instance = fake_instance.fake_instance_obj(self.ctx, node=node_uuid)
@@ -668,6 +699,7 @@ class IronicDriverTestCase(test.NoDBTestCase):
fake_looping_call.start.assert_called_once_with(
interval=CONF.ironic.api_retry_interval)
fake_looping_call.wait.assert_called_once_with()
+ mock_sb_options.assert_called_once_with(self.ctx, instance, node_uuid)
@mock.patch.object(ironic_driver.IronicDriver, '_generate_configdrive')
@mock.patch.object(configdrive, 'required_by')
@@ -720,14 +752,61 @@ class IronicDriverTestCase(test.NoDBTestCase):
self.driver.spawn, self.ctx, instance, None, [], None)
mock_destroy.assert_called_once_with(self.ctx, instance, None)
+ @mock.patch.object(FAKE_CLIENT, 'node')
+ def test__get_switch_boot_options(self, mock_node):
+ node_uuid = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee'
+ node = ironic_utils.get_test_node(
+ driver='fake', uuid=node_uuid,
+ instance_info={
+ 'multiboot': True,
+ 'multiboot_info': {'elements': [
+ {'image_name': "name_1"},
+ {'image_name': "name_2"},
+ ]}}
+ )
+ mock_node.get.return_value = node
+ instance = fake_instance.fake_instance_obj(
+ self.ctx, node=node_uuid, expected_attrs=('metadata',))
+
+ self.driver._get_switch_boot_options(self.ctx,
+ instance, node_uuid)
+
+ exp_meta = {'available_images': str(['name_1', 'name_2'])}
+ self.assertEqual(exp_meta, instance.metadata)
+
+ @mock.patch.object(FAKE_CLIENT, 'node')
+ def test__get_switch_boot_options_not_multiboot(self, mock_node):
+ node_uuid = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee'
+ node = ironic_utils.get_test_node(
+ driver='fake', uuid=node_uuid,
+ instance_info={}
+ )
+ mock_node.get.return_value = node
+ instance = fake_instance.fake_instance_obj(
+ self.ctx, node=node_uuid, expected_attrs=('metadata',))
+
+ self.driver._get_switch_boot_options(self.ctx,
+ instance, node_uuid)
+
+ self.assertEqual({}, instance.metadata)
+
+ @mock.patch.object(ironic_driver.IronicDriver,
+ "_get_deploy_config_options")
@mock.patch.object(FAKE_CLIENT.node, 'update')
- def test__add_driver_fields_good(self, mock_update):
+ def test__add_driver_fields_good(self, mock_update,
+ mock_get_depl_conf_opts):
node = ironic_utils.get_test_node(driver='fake')
- instance = fake_instance.fake_instance_obj(self.ctx,
- node=node.uuid)
+ instance = fake_instance.fake_instance_obj(
+ self.ctx,
+ node=node.uuid,
+ expected_attrs=('metadata',))
image_meta = ironic_utils.get_test_image_meta()
+ mock_get_depl_conf_opts.return_value = {'foo': 'bar123'}
+ instance['metadata']['driver_actions'] = {'bar': 'foo123'}
flavor = ironic_utils.get_test_flavor()
+
self.driver._add_driver_fields(node, instance, image_meta, flavor)
+
expected_patch = [{'path': '/instance_info/image_source', 'op': 'add',
'value': image_meta['id']},
{'path': '/instance_info/root_gb', 'op': 'add',
@@ -735,21 +814,96 @@ class IronicDriverTestCase(test.NoDBTestCase):
{'path': '/instance_info/swap_mb', 'op': 'add',
'value': str(flavor['swap'])},
{'path': '/instance_uuid', 'op': 'add',
- 'value': instance.uuid}]
+ 'value': instance.uuid},
+ {'path': '/instance_info/deploy_config_options',
+ 'op': 'add',
+ 'value': {'foo': 'bar123'}},
+ {'path': '/instance_info/driver_actions',
+ 'op': 'add',
+ 'value': {'bar': 'foo123'}},
+ ]
mock_update.assert_called_once_with(node.uuid, expected_patch)
@mock.patch.object(FAKE_CLIENT.node, 'update')
def test__add_driver_fields_fail(self, mock_update):
mock_update.side_effect = ironic_exception.BadRequest()
node = ironic_utils.get_test_node(driver='fake')
- instance = fake_instance.fake_instance_obj(self.ctx,
- node=node.uuid)
+ instance = fake_instance.fake_instance_obj(
+ self.ctx,
+ node=node.uuid,
+ expected_attrs=('metadata',))
image_meta = ironic_utils.get_test_image_meta()
flavor = ironic_utils.get_test_flavor()
self.assertRaises(exception.InstanceDeployFailure,
self.driver._add_driver_fields,
node, instance, image_meta, flavor)
+ def test__get_deploy_config_options_all_present(self):
+ node = ironic_utils.get_test_node(
+ driver='fake', driver_info={'deploy_config': "node-conf"})
+ image_meta = ironic_utils.get_test_image_meta(
+ deploy_config="image-conf")
+ instance = fake_instance.fake_instance_obj(
+ self.ctx, node=node.uuid, expected_attrs=('metadata',),
+ metadata={'deploy_config': "instance-conf"})
+
+ opts = self.driver._get_deploy_config_options(node, instance,
+ image_meta)
+
+ expected = {"node": "node-conf",
+ "image": "image-conf",
+ "instance": "instance-conf"
+ }
+ self.assertEqual(expected, opts)
+
+ def test__get_deploy_config_options_on_node_rebuild(self):
+ # In case of rebuild a set of options is also present in the node
+ # already. We take them, override with the new ones, and pass back.
+ node = ironic_utils.get_test_node(
+ driver='fake', driver_info={'deploy_config': "node-conf"},
+ instance_info={"deploy_config_options": {
+ "instance": "previous_inst_conf",
+ "image": "previous_image_conf",
+ }}
+ )
+ image_meta = ironic_utils.get_test_image_meta(
+ deploy_config="image-conf")
+ instance = fake_instance.fake_instance_obj(
+ self.ctx, node=node.uuid, expected_attrs=('metadata',))
+ opts = self.driver._get_deploy_config_options(node, instance,
+ image_meta)
+
+ expected = {"node": "node-conf",
+ "image": "image-conf",
+ "instance": "previous_inst_conf"
+ }
+ self.assertEqual(expected, opts)
+
+ def test__get_deploy_config_options_some_present(self):
+ node = ironic_utils.get_test_node(driver='fake')
+ image_meta = ironic_utils.get_test_image_meta()
+ instance = fake_instance.fake_instance_obj(
+ self.ctx, node=node.uuid, expected_attrs=('metadata',),
+ metadata={'deploy_config': "instance-conf"})
+
+ opts = self.driver._get_deploy_config_options(node, instance,
+ image_meta)
+
+ expected = {"instance": "instance-conf"}
+ self.assertEqual(expected, opts)
+
+ def test__get_deploy_config_options_none_present(self):
+ node = ironic_utils.get_test_node(driver='fake')
+ image_meta = ironic_utils.get_test_image_meta()
+ instance = fake_instance.fake_instance_obj(
+ self.ctx, node=node.uuid, expected_attrs=('metadata',))
+
+ opts = self.driver._get_deploy_config_options(node, instance,
+ image_meta)
+
+ expected = {}
+ self.assertEqual(expected, opts)
+
@mock.patch.object(FAKE_CLIENT.node, 'update')
def test__cleanup_deploy_good_with_flavor(self, mock_update):
node = ironic_utils.get_test_node(driver='fake',
@@ -781,8 +935,10 @@ class IronicDriverTestCase(test.NoDBTestCase):
node = ironic_utils.get_test_node(driver='fake',
instance_uuid=self.instance_uuid)
flavor = ironic_utils.get_test_flavor(extra_specs={})
- instance = fake_instance.fake_instance_obj(self.ctx,
- node=node.uuid)
+ instance = fake_instance.fake_instance_obj(
+ self.ctx,
+ node=node.uuid,
+ expected_attrs=('metadata',))
instance.flavor = flavor
self.assertRaises(exception.InstanceTerminationFailure,
self.driver._cleanup_deploy,
@@ -796,7 +952,8 @@ class IronicDriverTestCase(test.NoDBTestCase):
node_uuid = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee'
node = ironic_utils.get_test_node(driver='fake', uuid=node_uuid)
flavor = ironic_utils.get_test_flavor()
- instance = fake_instance.fake_instance_obj(self.ctx, node=node_uuid)
+ instance = fake_instance.fake_instance_obj(
+ self.ctx, node=node_uuid, expected_attrs=('metadata',))
instance.flavor = flavor
mock_node.validate.return_value = ironic_utils.get_test_validation(
@@ -821,7 +978,8 @@ class IronicDriverTestCase(test.NoDBTestCase):
node_uuid = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee'
node = ironic_utils.get_test_node(driver='fake', uuid=node_uuid)
flavor = ironic_utils.get_test_flavor()
- instance = fake_instance.fake_instance_obj(self.ctx, node=node_uuid)
+ instance = fake_instance.fake_instance_obj(
+ self.ctx, node=node_uuid, expected_attrs=('metadata',))
instance.flavor = flavor
mock_node.get.return_value = node
mock_node.validate.return_value = ironic_utils.get_test_validation()
@@ -851,7 +1009,8 @@ class IronicDriverTestCase(test.NoDBTestCase):
node_uuid = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee'
node = ironic_utils.get_test_node(driver='fake', uuid=node_uuid)
flavor = ironic_utils.get_test_flavor()
- instance = fake_instance.fake_instance_obj(self.ctx, node=node_uuid)
+ instance = fake_instance.fake_instance_obj(
+ self.ctx, node=node_uuid, expected_attrs=('metadata',))
instance.flavor = flavor
image_meta = ironic_utils.get_test_image_meta()
@@ -880,7 +1039,8 @@ class IronicDriverTestCase(test.NoDBTestCase):
node_uuid = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee'
node = ironic_utils.get_test_node(driver='fake', uuid=node_uuid)
flavor = ironic_utils.get_test_flavor()
- instance = fake_instance.fake_instance_obj(self.ctx, node=node_uuid)
+ instance = fake_instance.fake_instance_obj(
+ self.ctx, node=node_uuid, expected_attrs=('metadata',))
instance.flavor = flavor
image_meta = ironic_utils.get_test_image_meta()
@@ -912,7 +1072,8 @@ class IronicDriverTestCase(test.NoDBTestCase):
fake_net_info = utils.get_test_network_info()
node = ironic_utils.get_test_node(driver='fake', uuid=node_uuid)
flavor = ironic_utils.get_test_flavor()
- instance = fake_instance.fake_instance_obj(self.ctx, node=node_uuid)
+ instance = fake_instance.fake_instance_obj(
+ self.ctx, node=node_uuid, expected_attrs=('metadata',))
instance.flavor = flavor
image_meta = ironic_utils.get_test_image_meta()
@@ -945,7 +1106,8 @@ class IronicDriverTestCase(test.NoDBTestCase):
node_uuid = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee'
node = ironic_utils.get_test_node(driver='fake', uuid=node_uuid)
flavor = ironic_utils.get_test_flavor(ephemeral_gb=1)
- instance = fake_instance.fake_instance_obj(self.ctx, node=node_uuid)
+ instance = fake_instance.fake_instance_obj(
+ self.ctx, node=node_uuid, expected_attrs=('metadata',))
instance.flavor = flavor
mock_node.get_by_instance_uuid.return_value = node
mock_node.set_provision_state.return_value = mock.MagicMock()
@@ -957,12 +1119,12 @@ class IronicDriverTestCase(test.NoDBTestCase):
@mock.patch.object(FAKE_CLIENT, 'node')
@mock.patch.object(ironic_driver.IronicDriver, '_cleanup_deploy')
- def test_destroy(self, mock_cleanup_deploy, mock_node):
+ def _test_destroy(self, state, mock_cleanup_deploy, mock_node):
node_uuid = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee'
network_info = 'foo'
node = ironic_utils.get_test_node(driver='fake', uuid=node_uuid,
- provision_state=ironic_states.ACTIVE)
+ provision_state=state)
instance = fake_instance.fake_instance_obj(self.ctx, node=node_uuid)
def fake_set_provision_state(*_):
@@ -971,29 +1133,22 @@ class IronicDriverTestCase(test.NoDBTestCase):
mock_node.get_by_instance_uuid.return_value = node
mock_node.set_provision_state.side_effect = fake_set_provision_state
self.driver.destroy(self.ctx, instance, network_info, None)
- mock_node.set_provision_state.assert_called_once_with(node_uuid,
- 'deleted')
+
mock_node.get_by_instance_uuid.assert_called_with(instance.uuid)
mock_cleanup_deploy.assert_called_with(self.ctx, node,
instance, network_info)
- @mock.patch.object(FAKE_CLIENT, 'node')
- @mock.patch.object(ironic_driver.IronicDriver, '_cleanup_deploy')
- def test_destroy_ignore_unexpected_state(self, mock_cleanup_deploy,
- mock_node):
- node_uuid = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee'
- network_info = 'foo'
+ # For states that makes sense check if set_provision_state has
+ # been called
+ if state in ironic_driver._UNPROVISION_STATES:
+ mock_node.set_provision_state.assert_called_once_with(
+ node_uuid, 'deleted')
+ else:
+ self.assertFalse(mock_node.set_provision_state.called)
- node = ironic_utils.get_test_node(driver='fake', uuid=node_uuid,
- provision_state=ironic_states.DELETING)
- instance = fake_instance.fake_instance_obj(self.ctx, node=node_uuid)
-
- mock_node.get_by_instance_uuid.return_value = node
- self.driver.destroy(self.ctx, instance, network_info, None)
- self.assertFalse(mock_node.set_provision_state.called)
- mock_node.get_by_instance_uuid.assert_called_with(instance.uuid)
- mock_cleanup_deploy.assert_called_with(self.ctx, node, instance,
- network_info)
+ def test_destroy(self):
+ for state in ironic_states.PROVISION_STATE_LIST:
+ self._test_destroy(state)
@mock.patch.object(FAKE_CLIENT, 'node')
@mock.patch.object(ironic_driver.IronicDriver, '_cleanup_deploy')
@@ -1287,6 +1442,7 @@ class IronicDriverTestCase(test.NoDBTestCase):
self.driver.refresh_instance_security_rules(fake_group)
mock_risr.assert_called_once_with(fake_group)
+ @mock.patch.object(ironic_driver.IronicDriver, '_get_switch_boot_options')
@mock.patch.object(ironic_driver.IronicDriver, '_wait_for_active')
@mock.patch.object(loopingcall, 'FixedIntervalLoopingCall')
@mock.patch.object(FAKE_CLIENT.node, 'set_provision_state')
@@ -1295,7 +1451,7 @@ class IronicDriverTestCase(test.NoDBTestCase):
@mock.patch.object(objects.Instance, 'save')
def _test_rebuild(self, mock_save, mock_get, mock_driver_fields,
mock_set_pstate, mock_looping, mock_wait_active,
- preserve=False):
+ mock_sb_options, preserve=False):
node_uuid = uuidutils.generate_uuid()
node = ironic_utils.get_test_node(uuid=node_uuid,
instance_uuid=self.instance_uuid,
@@ -1306,10 +1462,12 @@ class IronicDriverTestCase(test.NoDBTestCase):
flavor_id = 5
flavor = objects.Flavor(flavor_id=flavor_id, name='baremetal')
- instance = fake_instance.fake_instance_obj(self.ctx,
- uuid=self.instance_uuid,
- node=node_uuid,
- instance_type_id=flavor_id)
+ instance = fake_instance.fake_instance_obj(
+ self.ctx,
+ uuid=self.instance_uuid,
+ node=node_uuid,
+ instance_type_id=flavor_id,
+ expected_attrs=('metadata',))
instance.flavor = flavor
fake_looping_call = FakeLoopingCall()
@@ -1333,6 +1491,7 @@ class IronicDriverTestCase(test.NoDBTestCase):
fake_looping_call.start.assert_called_once_with(
interval=CONF.ironic.api_retry_interval)
fake_looping_call.wait.assert_called_once_with()
+ mock_sb_options.assert_called_once_with(self.ctx, instance, node_uuid)
def test_rebuild_preserve_ephemeral(self):
self._test_rebuild(preserve=True)
@@ -1356,10 +1515,12 @@ class IronicDriverTestCase(test.NoDBTestCase):
flavor_id = 5
flavor = objects.Flavor(flavor_id=flavor_id, name='baremetal')
- instance = fake_instance.fake_instance_obj(self.ctx,
- uuid=self.instance_uuid,
- node=node_uuid,
- instance_type_id=flavor_id)
+ instance = fake_instance.fake_instance_obj(
+ self.ctx,
+ uuid=self.instance_uuid,
+ node=node_uuid,
+ instance_type_id=flavor_id,
+ expected_attrs=('metadata',))
instance.flavor = flavor
exceptions = [
@@ -1375,6 +1536,305 @@ class IronicDriverTestCase(test.NoDBTestCase):
injected_files=None, admin_password=None, bdms=None,
detach_block_devices=None, attach_block_devices=None)
+ @mock.patch.object(ironic_driver.IronicDriver, '_do_rebuild')
+ @mock.patch.object(ironic_driver.IronicDriver, '_get_switch_boot_options')
+ @mock.patch.object(ironic_driver.IronicDriver, '_wait_for_active')
+ @mock.patch.object(loopingcall, 'FixedIntervalLoopingCall')
+ @mock.patch.object(FAKE_CLIENT.node, 'set_provision_state')
+ @mock.patch.object(ironic_driver.IronicDriver, '_add_driver_fields')
+ @mock.patch.object(FAKE_CLIENT.node, 'get')
+ @mock.patch.object(objects.Instance, 'save')
+ def test_rebuild_multiboot_force_rebuild(self, mock_save, mock_get,
+ mock_driver_fields,
+ mock_set_pstate, mock_looping,
+ mock_wait_active,
+ mock_sb_options, rebuild_mock):
+ node_uuid = uuidutils.generate_uuid()
+ node = ironic_utils.get_test_node(uuid=node_uuid,
+ instance_uuid=self.instance_uuid,
+ instance_type_id=5,
+ instance_info={'multiboot': True})
+ mock_get.return_value = node
+
+ image_meta = ironic_utils.get_test_image_meta()
+ flavor_id = 5
+ flavor = objects.Flavor(flavor_id=flavor_id, name='baremetal')
+
+ instance = fake_instance.fake_instance_obj(
+ self.ctx,
+ uuid=self.instance_uuid,
+ node=node_uuid,
+ instance_type_id=flavor_id,
+ expected_attrs=('metadata',),
+ metadata={'force_rebuild': True})
+ instance.flavor = flavor
+
+ fake_looping_call = FakeLoopingCall()
+ mock_looping.return_value = fake_looping_call
+
+ self.driver.rebuild(
+ context=self.ctx, instance=instance, image_meta=image_meta,
+ injected_files=None, admin_password=None, bdms=None,
+ detach_block_devices=None, attach_block_devices=None,
+ preserve_ephemeral=False)
+
+ rebuild_mock.assert_called_once_with(
+ self.ctx, FAKE_CLIENT_WRAPPER, node, instance, image_meta,
+ None,
+ None, None, None,
+ None, network_info=None,
+ recreate=False,
+ block_device_info=None,
+ preserve_ephemeral=False)
+
+ @mock.patch.object(FAKE_CLIENT.node, 'get')
+ @mock.patch.object(ironic_driver.IronicDriver, '_do_switch_boot_device')
+ @mock.patch.object(objects.Instance, 'save')
+ def test_rebuild_multiboot_switch_boot(self, mock_save,
+ mock_sb, mock_get):
+ node_uuid = uuidutils.generate_uuid()
+ node = ironic_utils.get_test_node(uuid=node_uuid,
+ instance_uuid=self.instance_uuid,
+ instance_type_id=5,
+ instance_info={'multiboot': True})
+ mock_get.return_value = node
+ image_meta = ironic_utils.get_test_image_meta()
+ flavor_id = 5
+ instance = fake_instance.fake_instance_obj(
+ self.ctx,
+ uuid=self.instance_uuid,
+ node=node_uuid,
+ instance_type_id=flavor_id,
+ expected_attrs=('metadata',))
+
+ self.driver.rebuild(
+ context=self.ctx, instance=instance, image_meta=image_meta,
+ injected_files=None, admin_password=None, bdms=None,
+ detach_block_devices=None, attach_block_devices=None,
+ preserve_ephemeral=False)
+
+ mock_sb.assert_called_once_with(self.ctx, FAKE_CLIENT_WRAPPER, node,
+ instance, image_meta)
+
+ @mock.patch.object(FAKE_CLIENT.node, 'vendor_passthru')
+ @mock.patch.object(FAKE_CLIENT.node, 'set_power_state')
+ @mock.patch.object(FAKE_CLIENT.node, 'update')
+ @mock.patch.object(objects.Instance, 'save')
+ def test__do_switch_boot_device(self, mock_save, upd_mock,
+ sp_mock, vp_mock):
+ node_uuid = uuidutils.generate_uuid()
+ node = ironic_utils.get_test_node(uuid=node_uuid,
+ instance_uuid=self.instance_uuid,
+ instance_type_id=5,
+ instance_info={'multiboot': True})
+
+ image_meta = ironic_utils.get_test_image_meta()
+ flavor_id = 5
+ instance = fake_instance.fake_instance_obj(
+ self.ctx,
+ uuid=self.instance_uuid,
+ node=node_uuid,
+ instance_type_id=flavor_id,
+ expected_attrs=('metadata',),
+ metadata={'sb_key': 'key1', 'sb_user': 'usr1', 'extras': '123'})
+
+ self.driver._do_switch_boot_device(
+ self.ctx, FAKE_CLIENT_WRAPPER,
+ node, instance, image_meta)
+
+ vp_mock.assert_called_once_with(node_uuid, 'switch_boot',
+ {'image': image_meta['id'],
+ 'ssh_user': 'usr1',
+ 'ssh_key': 'key1'})
+ sp_mock.assert_called_once_with(node_uuid, 'reboot')
+ upd_mock.assert_called_once_with(
+ node_uuid, [{'path': '/instance_info/image_source', 'op': 'add',
+ 'value': image_meta['id']}])
+
+ @mock.patch.object(FAKE_CLIENT.node, 'vendor_passthru')
+ @mock.patch.object(FAKE_CLIENT.node, 'set_power_state')
+ @mock.patch.object(FAKE_CLIENT.node, 'update')
+ @mock.patch.object(objects.Instance, 'save')
+ def test__do_switch_boot_device_no_key(self, mock_save, upd_mock,
+ sp_mock, vp_mock):
+ node_uuid = uuidutils.generate_uuid()
+ node = ironic_utils.get_test_node(
+ uuid=node_uuid,
+ instance_uuid=self.instance_uuid,
+ instance_type_id=5,
+ instance_info={'multiboot': True,
+ 'image_source': 'original_image',
+ })
+
+ image_meta = ironic_utils.get_test_image_meta()
+ flavor_id = 5
+ instance = fake_instance.fake_instance_obj(
+ self.ctx,
+ uuid=self.instance_uuid,
+ node=node_uuid,
+ instance_type_id=flavor_id,
+ expected_attrs=('metadata',),
+ metadata={'sb_user': 'usr1'})
+
+ self.driver._do_switch_boot_device(
+ self.ctx, FAKE_CLIENT_WRAPPER,
+ node, instance, image_meta)
+
+ self.assertEqual(instance.image_ref, 'original_image')
+ self.assertIn('Invalid metadata',
+ instance.metadata['switch_boot_error'])
+ mock_save.assert_called_once_with()
+ vp_mock.assert_has_calls([])
+ sp_mock.assert_has_calls([])
+ upd_mock.assert_has_calls([])
+
+ @mock.patch.object(FAKE_CLIENT.node, 'vendor_passthru')
+ @mock.patch.object(FAKE_CLIENT.node, 'set_power_state')
+ @mock.patch.object(FAKE_CLIENT.node, 'update')
+ @mock.patch.object(objects.Instance, 'save')
+ def test__do_switch_boot_device_not_supported_by_driver(
+ self, mock_save, upd_mock, sp_mock, vp_mock):
+ node_uuid = uuidutils.generate_uuid()
+ node = ironic_utils.get_test_node(
+ uuid=node_uuid,
+ instance_uuid=self.instance_uuid,
+ instance_type_id=5,
+ instance_info={'multiboot': True,
+ 'image_source': 'original_image',
+ })
+
+ image_meta = ironic_utils.get_test_image_meta()
+ flavor_id = 5
+ instance = fake_instance.fake_instance_obj(
+ self.ctx,
+ uuid=self.instance_uuid,
+ node=node_uuid,
+ instance_type_id=flavor_id,
+ expected_attrs=('metadata',),
+ metadata={'sb_key': 'key1', 'sb_user': 'usr1'})
+
+ vp_mock.side_effect = ironic_exception.BadRequest()
+
+ self.driver._do_switch_boot_device(
+ self.ctx, FAKE_CLIENT_WRAPPER,
+ node, instance, image_meta)
+
+ self.assertEqual(instance.image_ref, 'original_image')
+ self.assertIn('Bad Request',
+ instance.metadata['switch_boot_error'])
+ mock_save.assert_called_once_with()
+ sp_mock.assert_has_calls([])
+ upd_mock.assert_has_calls([])
+
+ @mock.patch.object(FAKE_CLIENT.node, 'vendor_passthru')
+ @mock.patch.object(FAKE_CLIENT.node, 'set_power_state')
+ @mock.patch.object(FAKE_CLIENT.node, 'update')
+ @mock.patch.object(objects.Instance, 'save')
+ def test__do_switch_boot_device_already_in_desired_device(
+ self, mock_save, upd_mock, sp_mock, vp_mock):
+ node_uuid = uuidutils.generate_uuid()
+ node = ironic_utils.get_test_node(
+ uuid=node_uuid,
+ instance_uuid=self.instance_uuid,
+ instance_type_id=5,
+ instance_info={'multiboot': True,
+ 'image_source': 'original_image',
+ })
+
+ image_meta = ironic_utils.get_test_image_meta()
+ flavor_id = 5
+ instance = fake_instance.fake_instance_obj(
+ self.ctx,
+ uuid=self.instance_uuid,
+ node=node_uuid,
+ instance_type_id=flavor_id,
+ expected_attrs=('metadata',),
+ metadata={'sb_key': 'key1', 'sb_user': 'usr1'})
+
+ vp_mock.side_effect = ironic_exception.BadRequest(
+ message="Node 123 Already in desired boot device.")
+
+ self.driver._do_switch_boot_device(
+ self.ctx, FAKE_CLIENT_WRAPPER,
+ node, instance, image_meta)
+
+ mock_save.assert_has_calls([])
+ sp_mock.assert_has_calls([])
+ upd_mock.assert_has_calls([])
+
+ @mock.patch.object(FAKE_CLIENT.node, 'vendor_passthru')
+ @mock.patch.object(FAKE_CLIENT.node, 'set_power_state')
+ @mock.patch.object(FAKE_CLIENT.node, 'update')
+ @mock.patch.object(objects.Instance, 'save')
+ def test__do_switch_boot_device_reboot_error(
+ self, mock_save, upd_mock, sp_mock, vp_mock):
+ node_uuid = uuidutils.generate_uuid()
+ node = ironic_utils.get_test_node(
+ uuid=node_uuid,
+ instance_uuid=self.instance_uuid,
+ instance_type_id=5,
+ instance_info={'multiboot': True,
+ 'image_source': 'original_image',
+ })
+
+ image_meta = ironic_utils.get_test_image_meta()
+ flavor_id = 5
+ instance = fake_instance.fake_instance_obj(
+ self.ctx,
+ uuid=self.instance_uuid,
+ node=node_uuid,
+ instance_type_id=flavor_id,
+ expected_attrs=('metadata',),
+ metadata={'sb_key': 'key1', 'sb_user': 'usr1'})
+
+ sp_mock.side_effect = ironic_exception.BadRequest()
+
+ self.driver._do_switch_boot_device(
+ self.ctx, FAKE_CLIENT_WRAPPER,
+ node, instance, image_meta)
+
+ self.assertEqual(instance.image_ref, 'original_image')
+ self.assertIn('Bad Request',
+ instance.metadata['switch_boot_error'])
+ mock_save.assert_called_once_with()
+ upd_mock.assert_has_calls([])
+
+ @mock.patch.object(FAKE_CLIENT.node, 'vendor_passthru')
+ @mock.patch.object(FAKE_CLIENT.node, 'set_power_state')
+ @mock.patch.object(FAKE_CLIENT.node, 'update')
+ @mock.patch.object(objects.Instance, 'save')
+ def test__do_switch_boot_device_update_node_error(
+ self, mock_save, upd_mock, sp_mock, vp_mock):
+ node_uuid = uuidutils.generate_uuid()
+ node = ironic_utils.get_test_node(
+ uuid=node_uuid,
+ instance_uuid=self.instance_uuid,
+ instance_type_id=5,
+ instance_info={'multiboot': True,
+ 'image_source': 'original_image',
+ })
+
+ image_meta = ironic_utils.get_test_image_meta()
+ flavor_id = 5
+ instance = fake_instance.fake_instance_obj(
+ self.ctx,
+ uuid=self.instance_uuid,
+ node=node_uuid,
+ instance_type_id=flavor_id,
+ expected_attrs=('metadata',),
+ metadata={'sb_key': 'key1', 'sb_user': 'usr1'})
+
+ upd_mock.side_effect = ironic_exception.BadRequest()
+
+ self.driver._do_switch_boot_device(
+ self.ctx, FAKE_CLIENT_WRAPPER,
+ node, instance, image_meta)
+
+ self.assertEqual(instance.image_ref, 'original_image')
+ self.assertIn('Bad Request',
+ instance.metadata['switch_boot_error'])
+ mock_save.assert_called_once_with()
+
@mock.patch.object(instance_metadata, 'InstanceMetadata')
@mock.patch.object(configdrive, 'ConfigDriveBuilder')
diff --git a/nova/tests/unit/virt/ironic/utils.py b/nova/tests/unit/virt/ironic/utils.py
index d43f290..f3ec825 100644
--- a/nova/tests/unit/virt/ironic/utils.py
+++ b/nova/tests/unit/virt/ironic/utils.py
@@ -42,6 +42,7 @@ def get_test_node(**kw):
'driver': kw.get('driver', 'fake'),
'driver_info': kw.get('driver_info', {}),
'properties': kw.get('properties', {}),
+ 'instance_info': kw.get('instance_info', {}),
'reservation': kw.get('reservation'),
'maintenance': kw.get('maintenance', False),
'extra': kw.get('extra', {}),
@@ -72,7 +73,11 @@ def get_test_flavor(**kw):
def get_test_image_meta(**kw):
- return {'id': kw.get('id', 'cccccccc-cccc-cccc-cccc-cccccccccccc')}
+ return {'id': kw.get('id', 'cccccccc-cccc-cccc-cccc-cccccccccccc'),
+ 'properties': {
+ 'deploy_config': kw.get('deploy_config', ''),
+ 'driver_actions': kw.get('driver_actions', ''),
+ }}
class FakePortClient(object):
@@ -110,6 +115,9 @@ class FakeNodeClient(object):
def validate(self, node_uuid):
pass
+ def vendor_passthru(self, node_uuid, method, args):
+ pass
+
class FakeClient(object):
diff --git a/nova/virt/ironic/driver.py b/nova/virt/ironic/driver.py
index b21c782..81dcdba 100644
--- a/nova/virt/ironic/driver.py
+++ b/nova/virt/ironic/driver.py
@@ -40,6 +40,7 @@ from nova.compute import hv_type
from nova.compute import power_state
from nova.compute import task_states
from nova.compute import vm_mode
+from nova.compute import vm_states
from nova import context as nova_context
from nova import exception
from nova.i18n import _
@@ -107,6 +108,10 @@ _POWER_STATE_MAP = {
ironic_states.POWER_OFF: power_state.SHUTDOWN,
}
+_UNPROVISION_STATES = (ironic_states.ACTIVE, ironic_states.DEPLOYFAIL,
+ ironic_states.ERROR, ironic_states.DEPLOYWAIT,
+ ironic_states.DEPLOYING)
+
def map_power_state(state):
try:
@@ -326,6 +331,17 @@ class IronicDriver(virt_driver.ComputeDriver):
# Associate the node with an instance
patch.append({'path': '/instance_uuid', 'op': 'add',
'value': instance.uuid})
+
+ deploy_config_options = self._get_deploy_config_options(
+ node, instance, image_meta)
+ patch.append(
+ {'path': '/instance_info/deploy_config_options', 'op': 'add',
+ 'value': deploy_config_options})
+
+ patch.append(
+ {'path': '/instance_info/driver_actions', 'op': 'add',
+ 'value': instance.metadata.get('driver_actions', '')})
+
try:
self.ironicclient.call('node.update', node.uuid, patch)
except ironic.exc.BadRequest:
@@ -335,6 +351,13 @@ class IronicDriver(virt_driver.ComputeDriver):
LOG.error(msg)
raise exception.InstanceDeployFailure(msg)
+ def _update_driver_fields_after_switch_boot(self, context, node,
+ instance, image_meta):
+ patch = []
+ patch.append({'path': '/instance_info/image_source', 'op': 'add',
+ 'value': image_meta.get('id')})
+ self.ironicclient.call('node.update', node.uuid, patch)
+
def _cleanup_deploy(self, context, node, instance, network_info,
flavor=None):
if flavor is None:
@@ -358,6 +381,12 @@ class IronicDriver(virt_driver.ComputeDriver):
def _wait_for_active(self, ironicclient, instance):
"""Wait for the node to be marked as ACTIVE in Ironic."""
+ instance.refresh()
+ if (instance.task_state == task_states.DELETING or
+ instance.vm_state in (vm_states.ERROR, vm_states.DELETED)):
+ raise exception.InstanceDeployFailure(
+ _("Instance %s provisioning was aborted") % instance.uuid)
+
node = _validate_instance_and_node(ironicclient, instance)
if node.provision_state == ironic_states.ACTIVE:
# job is done
@@ -714,9 +743,19 @@ class IronicDriver(virt_driver.ComputeDriver):
# trigger the node deploy
try:
- self.ironicclient.call("node.set_provision_state", node_uuid,
- ironic_states.ACTIVE,
- configdrive=configdrive_value)
+ # NOTE(lobur): set_provision_state to
+ # ACTIVE, REBUILD, and switch_boot_device are the only Ironic API
+ # calls where the user context needs to be passed to Ironic. This
+ # is needed to be able to fetch a tenant-owned resources for
+ # deployment, e.g. deploy_config stored in Swift. The user should
+ # have admin role, otherwise this context will be replaced by a
+ # standard Ironic context (admin tenant). It is also required to
+ # have a standalone instance of ironicclient to make sure
+ # no other calls use user context cached in the client.
+ ironicclient = client_wrapper.IronicClientWrapper()
+ ironicclient.call("node.set_provision_state", node_uuid,
+ ironic_states.ACTIVE,
+ configdrive=configdrive_value)
except Exception as e:
with excutils.save_and_reraise_exception():
msg = (_LE("Failed to request Ironic to provision instance "
@@ -739,6 +778,17 @@ class IronicDriver(virt_driver.ComputeDriver):
{'instance': instance.uuid,
'node': node_uuid})
self.destroy(context, instance, network_info)
+ else:
+ self._get_switch_boot_options(context, instance, node_uuid)
+
+ def _get_switch_boot_options(self, context, instance, node_uuid):
+ # Reload node to see if multiboot flag appeared.
+ node = self.ironicclient.call("node.get", node_uuid)
+ if node.instance_info.get('multiboot'):
+ multiboot_meta = node.instance_info.get('multiboot_info', {})
+ available_images = [img['image_name'] for img in
+ multiboot_meta.get('elements', [])]
+ instance.metadata['available_images'] = str(available_images)
def _unprovision(self, ironicclient, instance, node):
"""This method is called from destroy() to unprovision
@@ -814,10 +864,7 @@ class IronicDriver(virt_driver.ComputeDriver):
# without raising any exceptions.
return
- if node.provision_state in (ironic_states.ACTIVE,
- ironic_states.DEPLOYFAIL,
- ironic_states.ERROR,
- ironic_states.DEPLOYWAIT):
+ if node.provision_state in _UNPROVISION_STATES:
self._unprovision(self.ironicclient, instance, node)
self._cleanup_deploy(context, node, instance, network_info)
@@ -1074,24 +1121,127 @@ class IronicDriver(virt_driver.ComputeDriver):
node_uuid = instance.node
node = self.ironicclient.call("node.get", node_uuid)
- self._add_driver_fields(node, instance, image_meta, instance.flavor,
- preserve_ephemeral)
+ # NOTE(lobur): set_provision_state to
+ # ACTIVE, REBUILD, and switch_boot_device are the only Ironic API
+ # calls where the user context needs to be passed to Ironic. This
+ # is needed to be able to fetch tenant-owned resources for
+ # deployment, e.g. deploy_config stored in Swift. The user should
+ # have admin role, otherwise this context will be replaced by a
+ # standard Ironic context (admin tenant). It is also required to
+ # have a standalone instance of ironicclient to make sure
+ # no other calls use user context cached in the client.
+ ironicclient = client_wrapper.IronicClientWrapper()
+
+ # To get a multiboot node rebuilt through the standard flow we
+ # require a separate force_rebuild flag in meta.
+ forced_rebuild = instance.metadata.pop('force_rebuild', False)
+
+ if node.instance_info.get('multiboot') and not forced_rebuild:
+ self._do_switch_boot_device(
+ context, ironicclient, node, instance, image_meta)
+ else:
+ self._do_rebuild(
+ context, ironicclient, node, instance, image_meta,
+ injected_files,
+ admin_password, bdms, detach_block_devices,
+ attach_block_devices, network_info=network_info,
+ recreate=recreate,
+ block_device_info=block_device_info,
+ preserve_ephemeral=preserve_ephemeral)
- # Trigger the node rebuild/redeploy.
+ self._get_switch_boot_options(context, instance, node_uuid)
+
+ def _do_switch_boot_device(self, context, ironicclient, node, instance,
+ image_meta):
+ old_image_ref = node.instance_info.get("image_source", "")
+ try:
+ sb_user, sb_key = self._get_switch_boot_user_key(instance.metadata)
+ args = dict(ssh_user=sb_user,
+ ssh_key=sb_key,
+ image=image_meta['id'])
+ ironicclient.call("node.vendor_passthru",
+ node.uuid, "switch_boot",
+ args)
+ self.ironicclient.call("node.set_power_state", node.uuid, 'reboot')
+ self._update_driver_fields_after_switch_boot(
+ context, node, instance, image_meta)
+ except (exception.InvalidMetadata, # Bad Nova API call
+ exception.NovaException, # Retry failed
+ ironic.exc.InternalServerError, # Validations
+ ironic.exc.BadRequest) as e: # Maintenance or no such API
+ # Ironic Vendor API always return 200/400/500, so the only way
+ # to check the error is introspecting its message.
+ if "Already in desired boot device" in six.text_type(e):
+ msg = (_("Ironic node %(node)s already has desired "
+ "boot device set.") % {'node': node.uuid})
+ LOG.warning(msg)
+ else:
+ # Rollback nova image ref
+ instance.image_ref = old_image_ref
+ # Cutting error msg to fit DB table.
+ instance.metadata['switch_boot_error'] = six.text_type(e)[:255]
+ instance.save()
+ msg = (_("Failed to switch Ironic %(node)s node boot "
+ "device: %(err)s")
+ % {'node': node.uuid, 'err': six.text_type(e)})
+ LOG.error(msg)
+
+ def _get_switch_boot_user_key(self, metadata):
+ sb_user = metadata.pop('sb_user', None)
+ sb_key = metadata.pop('sb_key', None)
+ if sb_user and sb_key:
+ return sb_user, sb_key
+ else:
+ raise exception.InvalidMetadata(
+ reason="To trigger switch boot device flow, both 'sb_user' "
+ "and 'sb_key' metadata params are required. To "
+ "trigger a standard rebuild flow, use "
+ "force_rebuild=True metadata flag.")
+
+ def _do_rebuild(self, context, ironicclient, node, instance, image_meta,
+ injected_files,
+ admin_password, bdms, detach_block_devices,
+ attach_block_devices, network_info=None,
+ recreate=False, block_device_info=None,
+ preserve_ephemeral=False):
+
+ self._add_driver_fields(node, instance, image_meta,
+ instance.flavor, preserve_ephemeral)
try:
- self.ironicclient.call("node.set_provision_state",
- node_uuid, ironic_states.REBUILD)
+
+ ironicclient.call("node.set_provision_state",
+ node.uuid, ironic_states.REBUILD)
except (exception.NovaException, # Retry failed
ironic.exc.InternalServerError, # Validations
ironic.exc.BadRequest) as e: # Maintenance
msg = (_("Failed to request Ironic to rebuild instance "
- "%(inst)s: %(reason)s") % {'inst': instance.uuid,
- 'reason': six.text_type(e)})
+ "%(inst)s: %(reason)s") %
+ {'inst': instance.uuid,
+ 'reason': six.text_type(e)})
raise exception.InstanceDeployFailure(msg)
- # Although the target provision state is REBUILD, it will actually go
- # to ACTIVE once the redeploy is finished.
+ # Although the target provision state is REBUILD, it will
+ # actually go to ACTIVE once the redeploy is finished.
timer = loopingcall.FixedIntervalLoopingCall(self._wait_for_active,
self.ironicclient,
instance)
timer.start(interval=CONF.ironic.api_retry_interval).wait()
+
+ def _get_deploy_config_options(self, node, instance, image_meta):
+ # Taking into account previous options, if any. This is to support
+ # rebuild flow where the user might or might not pass deploy_config
+ # reference. If no reference was passed, we'll take the option used for
+ # initial deployment.
+ res = node.instance_info.get('deploy_config_options', {})
+
+ curr_options = {
+ 'image': image_meta.get('properties', {}).get('deploy_config', ''),
+ 'instance': instance.metadata.get('deploy_config', ''),
+ 'node': node.driver_info.get('deploy_config', ''),
+ }
+ # Filter out empty ones
+ curr_options = {key: value for key, value in
+ curr_options.items() if value}
+ # Override previous by current.
+ res.update(curr_options)
+ return res
diff --git a/nova/virt/ironic/ironic_states.py b/nova/virt/ironic/ironic_states.py
index e521f16..a02ddcf 100644
--- a/nova/virt/ironic/ironic_states.py
+++ b/nova/virt/ironic/ironic_states.py
@@ -138,3 +138,13 @@ POWER_OFF = 'power off'
REBOOT = 'rebooting'
""" Node is rebooting. """
+
+##################
+# Helper constants
+##################
+
+PROVISION_STATE_LIST = (NOSTATE, MANAGEABLE, AVAILABLE, ACTIVE, DEPLOYWAIT,
+ DEPLOYING, DEPLOYFAIL, DEPLOYDONE, DELETING, DELETED,
+ CLEANING, CLEANFAIL, ERROR, REBUILD,
+ INSPECTING, INSPECTFAIL)
+""" A list of all provision states. """