Ironic: Support boot from Cinder volume
This enables Ironic to boot bare metal machines from Cinder volume. Ironic virt driver needs to pass the remote volume connection information down to Ironic when spawning a new bare metal instance requested to boot from a Cinder volume. This implements get_volume_connector method for the Ironic driver. It will get connector information from the Ironic service and pass it to Cinder's initialize_connection method for attached volumes. And then it puts the returned value into Ironic. This patch changes the required Ironic API version to 1.32 for using new API for volume resources. Co-Authored-By: Satoru Moriya <satoru.moriya.br@hitachi.com> Co-Authored-By: Hironori Shiina <shiina.hironori@jp.fujitsu.com> Change-Id: I319779af265684715f0142577a217ab66632bf4f Implements: blueprint ironic-boot-from-volume
This commit is contained in:
parent
19f78e2f5f
commit
3e1a3c9f82
@ -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
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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):
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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()
|
||||
|
@ -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):
|
||||
|
@ -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
|
||||
|
@ -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',
|
||||
|
@ -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.
|
Loading…
x
Reference in New Issue
Block a user