diff --git a/doc/source/user/support-matrix.ini b/doc/source/user/support-matrix.ini index ec6b6a834be0..9fa1c1d166dc 100644 --- a/doc/source/user/support-matrix.ini +++ b/doc/source/user/support-matrix.ini @@ -1005,7 +1005,7 @@ driver-impl-libvirt-lxc=complete driver-impl-libvirt-xen=complete driver-impl-vmware=complete driver-impl-hyperv=complete -driver-impl-ironic=missing +driver-impl-ironic=complete driver-impl-libvirt-vz-vm=partial driver-impl-libvirt-vz-ct=missing driver-impl-powervm=missing @@ -1052,7 +1052,7 @@ driver-impl-libvirt-lxc=complete driver-impl-libvirt-xen=complete driver-impl-vmware=complete driver-impl-hyperv=complete -driver-impl-ironic=missing +driver-impl-ironic=complete driver-impl-libvirt-vz-vm=complete driver-impl-libvirt-vz-ct=missing driver-impl-powervm=missing @@ -1074,7 +1074,7 @@ driver-impl-libvirt-lxc=complete driver-impl-libvirt-xen=complete driver-impl-vmware=missing driver-impl-hyperv=complete -driver-impl-ironic=missing +driver-impl-ironic=complete driver-impl-libvirt-vz-vm=complete driver-impl-libvirt-vz-ct=missing driver-impl-powervm=missing diff --git a/nova/tests/unit/virt/ironic/test_client_wrapper.py b/nova/tests/unit/virt/ironic/test_client_wrapper.py index e501de0153b6..c320b134e588 100644 --- a/nova/tests/unit/virt/ironic/test_client_wrapper.py +++ b/nova/tests/unit/virt/ironic/test_client_wrapper.py @@ -72,7 +72,7 @@ class IronicClientWrapperTestCase(test.NoDBTestCase): expected = {'session': 'session', 'max_retries': CONF.ironic.api_max_retries, 'retry_interval': CONF.ironic.api_retry_interval, - 'os_ironic_api_version': '1.29', + 'os_ironic_api_version': '1.32', 'ironic_url': None} mock_ir_cli.assert_called_once_with(1, **expected) diff --git a/nova/tests/unit/virt/ironic/test_driver.py b/nova/tests/unit/virt/ironic/test_driver.py index 50fa69288154..dd608f2f3030 100644 --- a/nova/tests/unit/virt/ironic/test_driver.py +++ b/nova/tests/unit/virt/ironic/test_driver.py @@ -25,6 +25,7 @@ from testtools import matchers from tooz import hashring as hash_ring from nova.api.metadata import base as instance_metadata +from nova import block_device from nova.compute import power_state as nova_states from nova.compute import task_states from nova.compute import vm_states @@ -36,10 +37,13 @@ from nova.objects import fields from nova import servicegroup from nova import test from nova.tests import fixtures +from nova.tests.unit import fake_block_device from nova.tests.unit import fake_instance from nova.tests.unit import matchers as nova_matchers from nova.tests.unit import utils from nova.tests.unit.virt.ironic import utils as ironic_utils +from nova.tests import uuidsentinel as uuids +from nova.virt import block_device as driver_block_device from nova.virt import configdrive from nova.virt import driver from nova.virt import fake @@ -944,13 +948,14 @@ class IronicDriverTestCase(test.NoDBTestCase): @mock.patch.object(objects.Instance, 'save') @mock.patch.object(loopingcall, 'FixedIntervalLoopingCall') @mock.patch.object(FAKE_CLIENT, 'node') + @mock.patch.object(ironic_driver.IronicDriver, '_add_volume_target_info') @mock.patch.object(ironic_driver.IronicDriver, '_wait_for_active') @mock.patch.object(ironic_driver.IronicDriver, '_add_instance_info_to_node') @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_aiitn, mock_wait_active, - mock_node, mock_looping, mock_save): + mock_avti, mock_node, mock_looping, mock_save): 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) @@ -974,7 +979,8 @@ class IronicDriverTestCase(test.NoDBTestCase): mock_node.validate.assert_called_once_with(node_uuid) mock_aiitn.assert_called_once_with(node, instance, test.MatchType(objects.ImageMeta), - fake_flavor) + fake_flavor, block_device_info=None) + mock_avti.assert_called_once_with(self.ctx, instance, None) mock_pvifs.assert_called_once_with(node, instance, None) mock_sf.assert_called_once_with(instance, None) mock_node.set_provision_state.assert_called_once_with(node_uuid, @@ -1011,6 +1017,7 @@ class IronicDriverTestCase(test.NoDBTestCase): @mock.patch.object(loopingcall, 'FixedIntervalLoopingCall') @mock.patch.object(FAKE_CLIENT, 'node') @mock.patch.object(ironic_driver.IronicDriver, 'destroy') + @mock.patch.object(ironic_driver.IronicDriver, '_add_volume_target_info') @mock.patch.object(ironic_driver.IronicDriver, '_wait_for_active') @mock.patch.object(ironic_driver.IronicDriver, '_add_instance_info_to_node') @@ -1018,7 +1025,7 @@ class IronicDriverTestCase(test.NoDBTestCase): @mock.patch.object(ironic_driver.IronicDriver, '_start_firewall') def test_spawn_destroyed_after_failure(self, mock_sf, mock_pvifs, mock_aiitn, mock_wait_active, - mock_destroy, mock_node, + mock_avti, mock_destroy, mock_node, mock_looping, mock_required_by): mock_required_by.return_value = False node_uuid = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee' @@ -1118,9 +1125,134 @@ class IronicDriverTestCase(test.NoDBTestCase): mock_update.side_effect = ironic_exception.BadRequest() self._test_remove_instance_info_from_node(mock_update) + def _create_fake_block_device_info(self): + bdm_dict = block_device.BlockDeviceDict({ + 'id': 1, 'instance_uuid': uuids.instance, + 'device_name': '/dev/sda', + 'source_type': 'volume', + 'volume_id': 'fake-volume-id-1', + 'connection_info': + '{"data":"fake_data",\ + "driver_volume_type":"fake_type"}', + 'boot_index': 0, + 'destination_type': 'volume' + }) + driver_bdm = driver_block_device.DriverVolumeBlockDevice( + fake_block_device.fake_bdm_object(self.ctx, bdm_dict)) + return { + 'block_device_mapping': [driver_bdm] + } + + @mock.patch.object(FAKE_CLIENT.volume_target, 'create') + def test__add_volume_target_info(self, mock_create): + node = ironic_utils.get_test_node(driver='fake') + instance = fake_instance.fake_instance_obj(self.ctx, node=node.uuid) + + block_device_info = self._create_fake_block_device_info() + self.driver._add_volume_target_info(self.ctx, instance, + block_device_info) + + expected_volume_type = 'fake_type' + expected_properties = 'fake_data' + expected_boot_index = 0 + + mock_create.assert_called_once_with(node_uuid=instance.node, + volume_type=expected_volume_type, + properties=expected_properties, + boot_index=expected_boot_index, + volume_id='fake-volume-id-1') + + @mock.patch.object(FAKE_CLIENT.volume_target, 'create') + def test__add_volume_target_info_empty_bdms(self, mock_create): + node = ironic_utils.get_test_node(driver='fake') + instance = fake_instance.fake_instance_obj(self.ctx, node=node.uuid) + + self.driver._add_volume_target_info(self.ctx, instance, None) + + self.assertFalse(mock_create.called) + + @mock.patch.object(FAKE_CLIENT.volume_target, 'create') + def test__add_volume_target_info_failures(self, mock_create): + node = ironic_utils.get_test_node(driver='fake') + instance = fake_instance.fake_instance_obj(self.ctx, node=node.uuid) + + block_device_info = self._create_fake_block_device_info() + + exceptions = [ + ironic_exception.BadRequest(), + ironic_exception.Conflict(), + ] + for e in exceptions: + mock_create.side_effect = e + self.assertRaises(exception.InstanceDeployFailure, + self.driver._add_volume_target_info, + self.ctx, instance, block_device_info) + + @mock.patch.object(FAKE_CLIENT.volume_target, 'delete') + @mock.patch.object(FAKE_CLIENT.node, 'list_volume_targets') + def test__cleanup_volume_target_info(self, mock_lvt, mock_delete): + node = ironic_utils.get_test_node(driver='fake') + instance = fake_instance.fake_instance_obj(self.ctx, node=node.uuid) + mock_lvt.return_value = [ironic_utils.get_test_volume_target( + uuid='fake_uuid')] + + self.driver._cleanup_volume_target_info(instance) + expected_volume_target_id = 'fake_uuid' + + mock_delete.assert_called_once_with(expected_volume_target_id) + + @mock.patch.object(FAKE_CLIENT.volume_target, 'delete') + @mock.patch.object(FAKE_CLIENT.node, 'list_volume_targets') + def test__cleanup_volume_target_info_empty_targets(self, mock_lvt, + mock_delete): + node = ironic_utils.get_test_node(driver='fake') + instance = fake_instance.fake_instance_obj(self.ctx, node=node.uuid) + mock_lvt.return_value = [] + + self.driver._cleanup_volume_target_info(instance) + + self.assertFalse(mock_delete.called) + + @mock.patch.object(FAKE_CLIENT.volume_target, 'delete') + @mock.patch.object(FAKE_CLIENT.node, 'list_volume_targets') + def test__cleanup_volume_target_info_not_found(self, mock_lvt, + mock_delete): + node = ironic_utils.get_test_node(driver='fake') + instance = fake_instance.fake_instance_obj(self.ctx, node=node.uuid) + mock_lvt.return_value = [ + ironic_utils.get_test_volume_target(uuid='fake_uuid1'), + ironic_utils.get_test_volume_target(uuid='fake_uuid2'), + ] + mock_delete.side_effect = [ironic_exception.NotFound('not found'), + None] + + self.driver._cleanup_volume_target_info(instance) + + self.assertEqual([mock.call('fake_uuid1'), mock.call('fake_uuid2')], + mock_delete.call_args_list) + + @mock.patch.object(FAKE_CLIENT.volume_target, 'delete') + @mock.patch.object(FAKE_CLIENT.node, 'list_volume_targets') + def test__cleanup_volume_target_info_bad_request(self, mock_lvt, + mock_delete): + node = ironic_utils.get_test_node(driver='fake') + instance = fake_instance.fake_instance_obj(self.ctx, node=node.uuid) + mock_lvt.return_value = [ + ironic_utils.get_test_volume_target(uuid='fake_uuid1'), + ironic_utils.get_test_volume_target(uuid='fake_uuid2'), + ] + mock_delete.side_effect = [ironic_exception.BadRequest('error'), + None] + + self.driver._cleanup_volume_target_info(instance) + + self.assertEqual([mock.call('fake_uuid1'), mock.call('fake_uuid2')], + mock_delete.call_args_list) + @mock.patch.object(configdrive, 'required_by') @mock.patch.object(FAKE_CLIENT, 'node') - def test_spawn_node_driver_validation_fail(self, mock_node, + @mock.patch.object(ironic_driver.IronicDriver, '_add_volume_target_info') + def test_spawn_node_driver_validation_fail(self, mock_avti, mock_node, mock_required_by): mock_required_by.return_value = False node_uuid = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee' @@ -1130,7 +1262,8 @@ class IronicDriverTestCase(test.NoDBTestCase): instance.flavor = flavor mock_node.validate.return_value = ironic_utils.get_test_validation( - power={'result': False}, deploy={'result': False}) + power={'result': False}, deploy={'result': False}, + storage={'result': False}) mock_node.get.return_value = node image_meta = ironic_utils.get_test_image_meta() @@ -1138,15 +1271,17 @@ class IronicDriverTestCase(test.NoDBTestCase): self.ctx, instance, image_meta, [], None) mock_node.get.assert_called_once_with( node_uuid, fields=ironic_driver._NODE_FIELDS) + mock_avti.assert_called_once_with(self.ctx, instance, None) mock_node.validate.assert_called_once_with(node_uuid) @mock.patch.object(configdrive, 'required_by') @mock.patch.object(FAKE_CLIENT, 'node') + @mock.patch.object(ironic_driver.IronicDriver, '_add_volume_target_info') @mock.patch.object(ironic_driver.IronicDriver, '_start_firewall') @mock.patch.object(ironic_driver.IronicDriver, '_plug_vifs') @mock.patch.object(ironic_driver.IronicDriver, '_cleanup_deploy') def test_spawn_node_prepare_for_deploy_fail(self, mock_cleanup_deploy, - mock_pvifs, mock_sf, + mock_pvifs, mock_sf, mock_avti, mock_node, mock_required_by): mock_required_by.return_value = False node_uuid = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee' @@ -1173,12 +1308,13 @@ class IronicDriverTestCase(test.NoDBTestCase): @mock.patch.object(configdrive, 'required_by') @mock.patch.object(objects.Instance, 'save') @mock.patch.object(FAKE_CLIENT, 'node') + @mock.patch.object(ironic_driver.IronicDriver, '_add_volume_target_info') @mock.patch.object(ironic_driver.IronicDriver, '_generate_configdrive') @mock.patch.object(ironic_driver.IronicDriver, '_start_firewall') @mock.patch.object(ironic_driver.IronicDriver, '_plug_vifs') def test_spawn_node_configdrive_fail(self, mock_pvifs, mock_sf, mock_configdrive, - mock_node, mock_save, + mock_avti, mock_node, mock_save, mock_required_by): mock_required_by.return_value = True node_uuid = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee' @@ -1206,11 +1342,12 @@ class IronicDriverTestCase(test.NoDBTestCase): @mock.patch.object(configdrive, 'required_by') @mock.patch.object(FAKE_CLIENT, 'node') + @mock.patch.object(ironic_driver.IronicDriver, '_add_volume_target_info') @mock.patch.object(ironic_driver.IronicDriver, '_start_firewall') @mock.patch.object(ironic_driver.IronicDriver, '_plug_vifs') @mock.patch.object(ironic_driver.IronicDriver, '_cleanup_deploy') def test_spawn_node_trigger_deploy_fail(self, mock_cleanup_deploy, - mock_pvifs, mock_sf, + mock_pvifs, mock_sf, mock_avti, mock_node, mock_required_by): mock_required_by.return_value = False node_uuid = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee' @@ -1234,11 +1371,12 @@ class IronicDriverTestCase(test.NoDBTestCase): @mock.patch.object(configdrive, 'required_by') @mock.patch.object(FAKE_CLIENT, 'node') + @mock.patch.object(ironic_driver.IronicDriver, '_add_volume_target_info') @mock.patch.object(ironic_driver.IronicDriver, '_start_firewall') @mock.patch.object(ironic_driver.IronicDriver, '_plug_vifs') @mock.patch.object(ironic_driver.IronicDriver, '_cleanup_deploy') def test_spawn_node_trigger_deploy_fail2(self, mock_cleanup_deploy, - mock_pvifs, mock_sf, + mock_pvifs, mock_sf, mock_avti, mock_node, mock_required_by): mock_required_by.return_value = False node_uuid = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee' @@ -1263,11 +1401,12 @@ class IronicDriverTestCase(test.NoDBTestCase): @mock.patch.object(configdrive, 'required_by') @mock.patch.object(loopingcall, 'FixedIntervalLoopingCall') @mock.patch.object(FAKE_CLIENT, 'node') + @mock.patch.object(ironic_driver.IronicDriver, '_add_volume_target_info') @mock.patch.object(ironic_driver.IronicDriver, '_start_firewall') @mock.patch.object(ironic_driver.IronicDriver, '_plug_vifs') @mock.patch.object(ironic_driver.IronicDriver, 'destroy') def test_spawn_node_trigger_deploy_fail3(self, mock_destroy, - mock_pvifs, mock_sf, + mock_pvifs, mock_sf, mock_avti, mock_node, mock_looping, mock_required_by): mock_required_by.return_value = False @@ -1295,12 +1434,14 @@ class IronicDriverTestCase(test.NoDBTestCase): @mock.patch.object(loopingcall, 'FixedIntervalLoopingCall') @mock.patch.object(objects.Instance, 'save') @mock.patch.object(FAKE_CLIENT, 'node') + @mock.patch.object(ironic_driver.IronicDriver, '_add_volume_target_info') @mock.patch.object(ironic_driver.IronicDriver, '_wait_for_active') @mock.patch.object(ironic_driver.IronicDriver, '_plug_vifs') @mock.patch.object(ironic_driver.IronicDriver, '_start_firewall') def test_spawn_sets_default_ephemeral_device(self, mock_sf, mock_pvifs, - mock_wait, mock_node, - mock_save, mock_looping, + mock_wait, mock_avti, + mock_node, mock_save, + mock_looping, mock_required_by): mock_required_by.return_value = False node_uuid = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee' @@ -1871,6 +2012,71 @@ class IronicDriverTestCase(test.NoDBTestCase): host_id = self.driver.network_binding_host_id(self.ctx, instance) self.assertIsNone(host_id) + @mock.patch.object(FAKE_CLIENT, 'node') + def test_get_volume_connector(self, mock_node): + node_uuid = uuids.node_uuid + node_props = {'cpu_arch': 'x86_64'} + node = ironic_utils.get_test_node(uuid=node_uuid, + properties=node_props) + connectors = [ironic_utils.get_test_volume_connector( + node_uuid=node_uuid, type='iqn', + connector_id='iqn.test'), + ironic_utils.get_test_volume_connector( + node_uuid=node_uuid, type='ip', + connector_id='1.2.3.4'), + ironic_utils.get_test_volume_connector( + node_uuid=node_uuid, type='wwnn', + connector_id='200010601'), + ironic_utils.get_test_volume_connector( + node_uuid=node_uuid, type='wwpn', + connector_id='200010605'), + ironic_utils.get_test_volume_connector( + node_uuid=node_uuid, type='wwpn', + connector_id='200010606')] + + expected_props = {'initiator': 'iqn.test', + 'ip': '1.2.3.4', + 'host': '1.2.3.4', + 'multipath': False, + 'wwnns': ['200010601'], + 'wwpns': ['200010605', '200010606'], + 'os_type': 'baremetal', + 'platform': 'x86_64'} + + mock_node.get.return_value = node + mock_node.list_volume_connectors.return_value = connectors + instance = fake_instance.fake_instance_obj(self.ctx, node=node_uuid) + props = self.driver.get_volume_connector(instance) + + self.assertEqual(expected_props, props) + mock_node.get.assert_called_once_with(node_uuid) + mock_node.list_volume_connectors.assert_called_once_with( + node_uuid, detail=True) + + @mock.patch.object(FAKE_CLIENT, 'node') + def test_get_volume_connector_no_ip(self, mock_node): + node_uuid = uuids.node_uuid + node_props = {'cpu_arch': 'x86_64'} + node = ironic_utils.get_test_node(uuid=node_uuid, + properties=node_props) + connectors = [ironic_utils.get_test_volume_connector( + node_uuid=node_uuid, type='iqn', + connector_id='iqn.test')] + expected_props = {'initiator': 'iqn.test', + 'multipath': False, + 'os_type': 'baremetal', + 'platform': 'x86_64'} + + mock_node.get.return_value = node + mock_node.list_volume_connectors.return_value = connectors + instance = fake_instance.fake_instance_obj(self.ctx, node=node_uuid) + props = self.driver.get_volume_connector(instance) + + self.assertEqual(expected_props, props) + mock_node.get.assert_called_once_with(node_uuid) + mock_node.list_volume_connectors.assert_called_once_with( + node_uuid, detail=True) + class IronicDriverSyncTestCase(IronicDriverTestCase): diff --git a/nova/tests/unit/virt/ironic/test_patcher.py b/nova/tests/unit/virt/ironic/test_patcher.py index 4def69488146..1758fe4706f6 100644 --- a/nova/tests/unit/virt/ironic/test_patcher.py +++ b/nova/tests/unit/virt/ironic/test_patcher.py @@ -143,3 +143,12 @@ class IronicDriverFieldsTestCase(test.NoDBTestCase): 'value': str(preserve), 'op': 'add', }] expected += self._expected_deploy_patch self.assertPatchEqual(expected, patch) + + def test_generic_get_deploy_patch_boot_from_volume(self): + node = ironic_utils.get_test_node(driver='fake') + expected = [patch for patch in self._expected_deploy_patch + if patch['path'] != '/instance_info/image_source'] + patch = patcher.create(node).get_deploy_patch( + self.instance, self.image_meta, self.flavor, + boot_from_volume=True) + self.assertPatchEqual(expected, patch) diff --git a/nova/tests/unit/virt/ironic/utils.py b/nova/tests/unit/virt/ironic/utils.py index 2e066adf4fd3..3066ec2d66cb 100644 --- a/nova/tests/unit/virt/ironic/utils.py +++ b/nova/tests/unit/virt/ironic/utils.py @@ -22,7 +22,8 @@ def get_test_validation(**kw): {'power': kw.get('power', {'result': True}), 'deploy': kw.get('deploy', {'result': True}), 'console': kw.get('console', True), - 'rescue': kw.get('rescue', True)})() + 'rescue': kw.get('rescue', True), + 'storage': kw.get('storage', {'result': True})})() def get_test_node(**kw): @@ -98,6 +99,31 @@ def get_test_vif(**kw): 'qbg_params': kw.get('qbg_params')} +def get_test_volume_connector(**kw): + return type('volume_connector', (object,), + {'uuid': kw.get('uuid', 'hhhhhhhh-qqqq-uuuu-mmmm-bbbbbbbbbbbb'), + 'node_uuid': kw.get('node_uuid', get_test_node().uuid), + 'type': kw.get('type', 'iqn'), + 'connector_id': kw.get('connector_id', 'iqn.test'), + 'extra': kw.get('extra', {}), + 'created_at': kw.get('created_at'), + 'updated_at': kw.get('updated_at')})() + + +def get_test_volume_target(**kw): + return type('volume_target', (object,), + {'uuid': kw.get('uuid', 'aaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee'), + 'node_uuid': kw.get('node_uuid', get_test_node().uuid), + 'volume_type': kw.get('volume_type', 'iscsi'), + 'properties': kw.get('properties', {}), + 'boot_index': kw.get('boot_index', 0), + 'volume_id': kw.get('volume_id', + 'fffffff-gggg-hhhh-iiii-jjjjjjjjjjjj'), + 'extra': kw.get('extra', {}), + 'created_at': kw.get('created_at'), + 'updated_at': kw.get('updated_at')})() + + def get_test_flavor(**kw): default_extra_specs = {'baremetal:deploy_kernel_id': 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', @@ -118,6 +144,16 @@ def get_test_image_meta(**kw): {'id': kw.get('id', 'cccccccc-cccc-cccc-cccc-cccccccccccc')}) +class FakeVolumeTargetClient(object): + + def create(self, node_uuid, driver_volume_type, target_properties, + boot_index): + pass + + def delete(self, volume_target_id): + pass + + class FakePortClient(object): def get(self, port_uuid): @@ -168,9 +204,13 @@ class FakeNodeClient(object): def inject_nmi(self, node_uuid): pass + def list_volume_targets(self, node_uuid, detail=False): + pass + class FakeClient(object): node = FakeNodeClient() port = FakePortClient() portgroup = FakePortgroupClient() + volume_target = FakeVolumeTargetClient() diff --git a/nova/virt/ironic/client_wrapper.py b/nova/virt/ironic/client_wrapper.py index 2bf83099db4d..9453adfcf27d 100644 --- a/nova/virt/ironic/client_wrapper.py +++ b/nova/virt/ironic/client_wrapper.py @@ -30,7 +30,7 @@ ironic = None IRONIC_GROUP = nova.conf.ironic.ironic_group # The API version required by the Ironic driver -IRONIC_API_VERSION = (1, 29) +IRONIC_API_VERSION = (1, 32) class IronicClientWrapper(object): diff --git a/nova/virt/ironic/driver.py b/nova/virt/ironic/driver.py index ea5fcb273267..ec30635d036b 100644 --- a/nova/virt/ironic/driver.py +++ b/nova/virt/ironic/driver.py @@ -25,6 +25,7 @@ import tempfile import time from oslo_log import log as logging +from oslo_serialization import jsonutils from oslo_service import loopingcall from oslo_utils import excutils from oslo_utils import importutils @@ -33,6 +34,7 @@ import six.moves.urllib.parse as urlparse from tooz import hashring as hash_ring from nova.api.metadata import base as instance_metadata +from nova import block_device from nova.compute import power_state from nova.compute import task_states from nova.compute import vm_states @@ -358,11 +360,17 @@ class IronicDriver(virt_driver.ComputeDriver): self.firewall_driver.unfilter_instance(instance, network_info) def _add_instance_info_to_node(self, node, instance, image_meta, flavor, - preserve_ephemeral=None): + preserve_ephemeral=None, + block_device_info=None): + + root_bdm = block_device.get_root_bdm( + virt_driver.block_device_info_get_mapping(block_device_info)) + boot_from_volume = root_bdm is not None patch = patcher.create(node).get_deploy_patch(instance, image_meta, flavor, - preserve_ephemeral) + preserve_ephemeral, + boot_from_volume) # Associate the node with an instance patch.append({'path': '/instance_uuid', 'op': 'add', @@ -393,7 +401,65 @@ class IronicDriver(virt_driver.ComputeDriver): {'node': node.uuid, 'instance': instance.uuid, 'reason': six.text_type(e)}) + def _add_volume_target_info(self, context, instance, block_device_info): + bdms = virt_driver.block_device_info_get_mapping(block_device_info) + + for bdm in bdms: + # TODO(TheJulia): In Queens, we should refactor the check below + # to something more elegent, as is_volume is not proxied through + # to the DriverVolumeBlockDevice object. Until then, we are + # checking the underlying object's status. + if not bdm._bdm_obj.is_volume: + continue + + connection_info = jsonutils.loads(bdm._bdm_obj.connection_info) + target_properties = connection_info['data'] + driver_volume_type = connection_info['driver_volume_type'] + + try: + self.ironicclient.call('volume_target.create', + node_uuid=instance.node, + volume_type=driver_volume_type, + properties=target_properties, + boot_index=bdm._bdm_obj.boot_index, + volume_id=bdm._bdm_obj.volume_id) + except (ironic.exc.BadRequest, ironic.exc.Conflict): + msg = (_("Failed to add volume target information of " + "volume %(volume)s on node %(node)s when " + "provisioning the instance") + % {'volume': bdm._bdm_obj.volume_id, + 'node': instance.node}) + LOG.error(msg, instance=instance) + raise exception.InstanceDeployFailure(msg) + + def _cleanup_volume_target_info(self, instance): + targets = self.ironicclient.call('node.list_volume_targets', + instance.node, detail=True) + for target in targets: + volume_target_id = target.uuid + try: + self.ironicclient.call('volume_target.delete', + volume_target_id) + except ironic.exc.NotFound: + LOG.debug("Volume target information %(target)s of volume " + "%(volume)s is already removed from node %(node)s", + {'target': volume_target_id, + 'volume': target.volume_id, + 'node': instance.node}, + instance=instance) + except ironic.exc.ClientException as e: + LOG.warning("Failed to remove volume target information " + "%(target)s of volume %(volume)s from node " + "%(node)s when unprovisioning the instance: " + "%(reason)s", + {'target': volume_target_id, + 'volume': target.volume_id, + 'node': instance.node, + 'reason': e}, + instance=instance) + def _cleanup_deploy(self, node, instance, network_info): + self._cleanup_volume_target_info(instance) self._unplug_vifs(node, instance, network_info) self._stop_firewall(instance, network_info) @@ -911,7 +977,7 @@ class IronicDriver(virt_driver.ComputeDriver): instance. :param network_info: Instance network information. :param block_device_info: Instance block device - information. Ignored by this driver. + information. """ LOG.debug('Spawn called for instance', instance=instance) @@ -926,7 +992,18 @@ class IronicDriver(virt_driver.ComputeDriver): node = self._get_node(node_uuid) flavor = instance.flavor - self._add_instance_info_to_node(node, instance, image_meta, flavor) + self._add_instance_info_to_node(node, instance, image_meta, flavor, + block_device_info=block_device_info) + + try: + self._add_volume_target_info(context, instance, block_device_info) + except Exception: + with excutils.save_and_reraise_exception(): + LOG.error("Error preparing deploy for instance " + "on baremetal node %(node)s.", + {'node': node_uuid}, + instance=instance) + self._cleanup_deploy(node, instance, network_info) # NOTE(Shrews): The default ephemeral device needs to be set for # services (like cloud-init) that depend on it being returned by the @@ -938,15 +1015,18 @@ class IronicDriver(virt_driver.ComputeDriver): # validate we are ready to do the deploy validate_chk = self.ironicclient.call("node.validate", node_uuid) if (not validate_chk.deploy.get('result') - or not validate_chk.power.get('result')): + or not validate_chk.power.get('result') + or not validate_chk.storage.get('result')): # something is wrong. undo what we have done self._cleanup_deploy(node, instance, network_info) raise exception.ValidationError(_( "Ironic node: %(id)s failed to validate." - " (deploy: %(deploy)s, power: %(power)s)") + " (deploy: %(deploy)s, power: %(power)s," + " storage: %(storage)s)") % {'id': node.uuid, 'deploy': validate_chk.deploy, - 'power': validate_chk.power}) + 'power': validate_chk.power, + 'storage': validate_chk.storage}) # prepare for the deploy try: @@ -1637,3 +1717,50 @@ class IronicDriver(virt_driver.ComputeDriver): 'node': node.uuid}, instance=instance) raise exception.ConsoleTypeUnavailable(console_type='serial') + + @property + def need_legacy_block_device_info(self): + return False + + def get_volume_connector(self, instance): + """Get connector information for the instance for attaching to volumes. + + Connector information is a dictionary representing the hardware + information that will be making the connection. This information + consists of properties for protocols supported by the hardware. + If the hardware supports iSCSI protocol, iSCSI initiator IQN is + included as follows:: + + { + 'ip': ip, + 'initiator': initiator, + 'host': hostname + } + + :param instance: nova instance + :returns: A connector information dictionary + """ + node = self.ironicclient.call("node.get", instance.node) + properties = self._parse_node_properties(node) + connectors = self.ironicclient.call("node.list_volume_connectors", + instance.node, detail=True) + values = {} + for conn in connectors: + values.setdefault(conn.type, []).append(conn.connector_id) + props = {} + + if values.get('ip'): + props['ip'] = props['host'] = values['ip'][0] + if values.get('iqn'): + props['initiator'] = values['iqn'][0] + if values.get('wwpn'): + props['wwpns'] = values['wwpn'] + if values.get('wwnn'): + props['wwnns'] = values['wwnn'] + props['platform'] = properties.get('cpu_arch') + props['os_type'] = 'baremetal' + + # Eventually it would be nice to be able to do multipath, but for now + # we should at least set the value to False. + props['multipath'] = False + return props diff --git a/nova/virt/ironic/patcher.py b/nova/virt/ironic/patcher.py index 651c7012c339..f212ce4d0f70 100644 --- a/nova/virt/ironic/patcher.py +++ b/nova/virt/ironic/patcher.py @@ -41,7 +41,7 @@ class GenericDriverFields(object): self.node = node def get_deploy_patch(self, instance, image_meta, flavor, - preserve_ephemeral=None): + preserve_ephemeral=None, boot_from_volume=False): """Build a patch to add the required fields to deploy a node. :param instance: the instance object. @@ -49,12 +49,15 @@ class GenericDriverFields(object): :param flavor: the flavor object. :param preserve_ephemeral: preserve_ephemeral status (bool) to be specified during rebuild. + :param boot_from_volume: True if node boots from volume. Then, + image_source is not passed to ironic. :returns: a json-patch with the fields that needs to be updated. """ patch = [] - patch.append({'path': '/instance_info/image_source', 'op': 'add', - 'value': image_meta.id}) + if not boot_from_volume: + patch.append({'path': '/instance_info/image_source', 'op': 'add', + 'value': image_meta.id}) patch.append({'path': '/instance_info/root_gb', 'op': 'add', 'value': str(instance.flavor.root_gb)}) patch.append({'path': '/instance_info/swap_mb', 'op': 'add', diff --git a/releasenotes/notes/bp-ironic-boot-from-volume-cfb98c733cf09a92.yaml b/releasenotes/notes/bp-ironic-boot-from-volume-cfb98c733cf09a92.yaml new file mode 100644 index 000000000000..539da2720b4d --- /dev/null +++ b/releasenotes/notes/bp-ironic-boot-from-volume-cfb98c733cf09a92.yaml @@ -0,0 +1,12 @@ +--- +features: + - | + Enables to launch an instance from an iscsi volume with ironic virt + driver. This feature requires an ironic service supporting API + version 1.32 or later, which is present in ironic releases > 8.0. + It also requires python-ironicclient >= 1.14.0. +upgrade: + - | + The required ironic API version is updated to 1.32. The ironic service + must be upgraded to an ironic release > 8.0 before nova is upgraded, + otherwise all ironic intergration will fail.