Merge "Ironic: Support boot from Cinder volume"

This commit is contained in:
Jenkins 2017-07-25 15:18:35 +00:00 committed by Gerrit Code Review
commit bed59a47b7
9 changed files with 425 additions and 28 deletions

View File

@ -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

View File

@ -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)

View File

@ -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):

View File

@ -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)

View File

@ -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()

View File

@ -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):

View File

@ -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

View File

@ -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',

View File

@ -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.