Merge "Ironic: Support boot from Cinder volume"
This commit is contained in:
commit
bed59a47b7
@ -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