Extend volume for libvirt network volumes (RBD)

Implement support for extending RBD attached volumes using the libvirt
network volume driver.

This adds a new parameter "requested_size" to the extend_volume method.
This is necessary because the new volume size can not be detected by
libvirt for network volumes. All other volume types currently
implementing the extend_volume call have a block device on the
hypervisor which needs to be updated and can be polled for it's new
size. For network volumes no such block device exists.

Alternatively this could be implemented without a new parameter by
calling into Ceph using os_brick to get the new size of the volume.
This would make the LibvirtNetVolumeDriver Ceph specific.

This also extends the logic to get the device_path for extending volumes
in the libvirt driver. This is necessary as network volumes don't have
the device path in the connection_info. The device_path is retrieved by
matching the connection_info serial (= volume UUID) against all guest
disks.

Co-Authored-By: Jose Castro Leon <jose.castro.leon@cern.ch>

Blueprint: extend-in-use-rbd-volumes

Change-Id: I5698e451861828a8b1240d046d1610d8d37ca5a2
This commit is contained in:
Gaudenz Steinlin 2018-09-11 16:09:27 +02:00 committed by Jose Castro Leon
parent c9dca64fa6
commit eab58069ea
21 changed files with 181 additions and 39 deletions

View File

@ -52,6 +52,7 @@ from oslo_service import periodic_task
from oslo_utils import excutils
from oslo_utils import strutils
from oslo_utils import timeutils
from oslo_utils import units
import six
from six.moves import range
@ -8048,7 +8049,8 @@ class ComputeManager(manager.Manager):
try:
self.driver.extend_volume(connection_info,
instance)
instance,
bdm.volume_size * units.Gi)
except Exception as ex:
LOG.warning('Extend volume failed, '
'volume_id=%(volume_id)s, reason: %(msg)s',

View File

@ -2965,6 +2965,7 @@ class ComputeManagerUnitTestCase(test.NoDBTestCase):
def test_extend_volume(self):
inst_obj = objects.Instance(id=3, uuid=uuids.instance)
connection_info = {'foo': 'bar'}
new_size = 20
bdm = objects.BlockDeviceMapping(
source_type='volume',
destination_type='volume',
@ -2982,13 +2983,13 @@ class ComputeManagerUnitTestCase(test.NoDBTestCase):
def do_test(bdm_save, bdm_get_by_vol_and_inst, extend_volume,
volume_api):
bdm_get_by_vol_and_inst.return_value = bdm
volume_api.get.return_value = {'size': 20}
volume_api.get.return_value = {'size': new_size}
self.compute.extend_volume(
self.context, inst_obj, uuids.volume_id)
bdm_save.assert_called_once_with()
extend_volume.assert_called_once_with(
connection_info, inst_obj)
connection_info, inst_obj, new_size * pow(1024, 3))
do_test()

View File

@ -7997,9 +7997,11 @@ class LibvirtConnTestCase(test.NoDBTestCase,
for state in (power_state.RUNNING, power_state.PAUSED):
guest.get_power_state = mock.Mock(return_value=state)
drvr.extend_volume(connection_info, instance)
drvr.extend_volume(connection_info,
instance, new_size_in_kb * 1024)
drvr._extend_volume.assert_called_with(connection_info,
instance)
instance,
new_size_in_kb * 1024)
guest.get_block_device.assert_called_with('/fake')
block_device.resize.assert_called_with(20480)
@ -8012,7 +8014,7 @@ class LibvirtConnTestCase(test.NoDBTestCase,
connection_info = {'driver_volume_type': 'fake'}
self.assertRaises(exception.ExtendVolumeNotSupported,
drvr.extend_volume,
connection_info, instance)
connection_info, instance, 0)
def test_extend_volume_disk_not_found(self):
drvr = libvirt_driver.LibvirtDriver(fake.FakeVirtAPI(), False)
@ -8031,7 +8033,7 @@ class LibvirtConnTestCase(test.NoDBTestCase,
drvr._host.get_guest = mock.Mock(return_value=guest)
drvr._extend_volume = mock.Mock(return_value=new_size_in_kb)
drvr.extend_volume(connection_info, instance)
drvr.extend_volume(connection_info, instance, new_size_in_kb * 1024)
def test_extend_volume_with_instance_not_found(self):
drvr = libvirt_driver.LibvirtDriver(fake.FakeVirtAPI(), False)
@ -8046,7 +8048,7 @@ class LibvirtConnTestCase(test.NoDBTestCase,
connection_info = {'driver_volume_type': 'fake'}
self.assertRaises(exception.InstanceNotFound,
drvr.extend_volume,
connection_info, instance)
connection_info, instance, 0)
def test_extend_volume_with_libvirt_error(self):
drvr = libvirt_driver.LibvirtDriver(fake.FakeVirtAPI(), False)
@ -8071,7 +8073,80 @@ class LibvirtConnTestCase(test.NoDBTestCase,
self.assertRaises(fakelibvirt.libvirtError,
drvr.extend_volume,
connection_info, instance)
connection_info, instance, new_size_in_kb * 1024)
def test_extend_volume_with_no_device_path_attribute(self):
drvr = libvirt_driver.LibvirtDriver(fake.FakeVirtAPI(), False)
instance = objects.Instance(**self.test_instance)
connection_info = {
'serial': '58a84f6d-3f0c-4e19-a0af-eb657b790657',
'driver_volume_type': 'fake',
'data': {'cluster_name': 'fake',
'auth_enabled': False,
'volume_id': '58a84f6d-3f0c-4e19-a0af-eb657b790657',
'access_mode': 'rw'}
}
new_size_in_kb = 20 * 1024 * 1024
guest = mock.Mock(spec='nova.virt.libvirt.guest.Guest')
# block_device
block_device = mock.Mock(
spec='nova.virt.libvirt.guest.BlockDevice')
block_device.resize = mock.Mock()
disk = mock.Mock(
spec='nova.virt.libvirt.config.LibvirtConfigGuestDisk',
serial='58a84f6d-3f0c-4e19-a0af-eb657b790657',
target_dev='vdb')
guest.get_block_device = mock.Mock(return_value=block_device)
guest.get_all_disks = mock.Mock(return_value=[disk])
drvr._host.get_guest = mock.Mock(return_value=guest)
drvr._extend_volume = mock.Mock(return_value=new_size_in_kb)
for state in (power_state.RUNNING, power_state.PAUSED):
guest.get_power_state = mock.Mock(return_value=state)
drvr.extend_volume(connection_info, instance,
new_size_in_kb * 1024)
drvr._extend_volume.assert_called_with(connection_info,
instance,
new_size_in_kb * 1024)
guest.get_block_device.assert_called_with('vdb')
block_device.resize.assert_called_with(20480)
def test_extend_volume_no_disk_found_by_serial(self):
drvr = libvirt_driver.LibvirtDriver(fake.FakeVirtAPI(), False)
instance = objects.Instance(**self.test_instance)
connection_info = {
'serial': '58a84f6d-3f0c-4e19-a0af-eb657b790657',
'driver_volume_type': 'fake',
'data': {'cluster_name': 'fake',
'auth_enabled': False,
'volume_id': '58a84f6d-3f0c-4e19-a0af-eb657b790657',
'access_mode': 'rw'}
}
new_size_in_kb = 20 * 1024 * 1024
guest = mock.Mock(spec='nova.virt.libvirt.guest.Guest')
# block_device
block_device = mock.Mock(
spec='nova.virt.libvirt.guest.BlockDevice')
block_device.resize = mock.Mock()
disk = mock.Mock(
spec='nova.virt.libvirt.config.LibvirtConfigGuestDisk',
serial='12345678-abcd-abcd-abcd-0123456789012',
target_dev='vdb')
guest.get_block_device = mock.Mock(return_value=block_device)
guest.get_all_disks = mock.Mock(return_value=[disk])
drvr._host.get_guest = mock.Mock(return_value=guest)
drvr._extend_volume = mock.Mock(return_value=new_size_in_kb)
guest.get_power_state = mock.Mock(return_value=power_state.RUNNING)
self.assertRaises(
exception.VolumeNotFound,
drvr.extend_volume,
connection_info,
instance,
new_size_in_kb * 1024
)
@mock.patch('os_brick.encryptors.get_encryption_metadata')
@mock.patch('nova.virt.libvirt.driver.LibvirtDriver._get_volume_encryptor')

View File

@ -68,13 +68,16 @@ class LibvirtFibreChannelVolumeDriverTestCase(
def test_extend_volume(self):
device_path = '/dev/fake-dev'
connection_info = {'data': {'device_path': device_path}}
requested_size = 1
libvirt_driver = fibrechannel.LibvirtFibreChannelVolumeDriver(
self.fake_host)
libvirt_driver.connector.extend_volume = mock.MagicMock(return_value=1)
libvirt_driver.connector.extend_volume = mock.MagicMock(
return_value=requested_size)
new_size = libvirt_driver.extend_volume(connection_info,
mock.sentinel.instance)
mock.sentinel.instance,
requested_size)
self.assertEqual(1, new_size)
self.assertEqual(requested_size, new_size)
libvirt_driver.connector.extend_volume.assert_called_once_with(
connection_info['data'])

View File

@ -64,12 +64,15 @@ class LibvirtISCSIVolumeDriverTestCase(
def test_extend_volume(self):
device_path = '/dev/fake-dev'
connection_info = {'data': {'device_path': device_path}}
requested_size = 1
libvirt_driver = iscsi.LibvirtISCSIVolumeDriver(self.fake_host)
libvirt_driver.connector.extend_volume = mock.MagicMock(return_value=1)
libvirt_driver.connector.extend_volume = mock.MagicMock(
return_value=requested_size)
new_size = libvirt_driver.extend_volume(connection_info,
mock.sentinel.instance)
mock.sentinel.instance,
requested_size)
self.assertEqual(1, new_size)
self.assertEqual(requested_size, new_size)
libvirt_driver.connector.extend_volume.assert_called_once_with(
connection_info['data'])

View File

@ -240,3 +240,16 @@ class LibvirtNetVolumeDriverTestCase(
tree.find('./auth/secret').get('uuid'))
libvirt_driver.disconnect_volume(connection_info,
mock.sentinel.instance)
def test_extend_volume(self):
device_path = '/dev/fake-dev'
connection_info = {'data': {'device_path': device_path}}
requested_size = 20 * pow(1024, 3) # 20GiB
libvirt_driver = net.LibvirtNetVolumeDriver(self.fake_host)
new_size = libvirt_driver.extend_volume(connection_info,
mock.sentinel.instance,
requested_size)
self.assertEqual(requested_size, new_size)

View File

@ -71,4 +71,5 @@ class LibvirtScaleIOVolumeDriverTestCase(
'extend_volume',
side_effect=brick_extend_vol):
self.assertEqual(extended_vol_size,
sio.extend_volume(conn, mock.sentinel.instance))
sio.extend_volume(conn, mock.sentinel.instance,
extended_vol_size))

View File

@ -127,32 +127,34 @@ class LibvirtStorPoolVolumeDriverTestCase(
ci_1 = self.conn_info('1')
ci_2 = self.conn_info('2')
rs_1 = ci_1['data']['real_size']
rs_2 = ci_2['data']['real_size']
self.assertRaises(MockStorPoolExc,
libvirt_driver.extend_volume,
ci_1, mock.sentinel.instance)
ci_1, mock.sentinel.instance, rs_1)
self.assertRaises(MockStorPoolExc,
libvirt_driver.extend_volume,
ci_2, mock.sentinel.instance)
ci_2, mock.sentinel.instance, rs_2)
libvirt_driver.connect_volume(ci_1, mock.sentinel.instance)
self.assertStorpoolAttached(('1',))
ns_1 = libvirt_driver.extend_volume(ci_1, mock.sentinel.instance)
ns_1 = libvirt_driver.extend_volume(ci_1, mock.sentinel.instance, rs_1)
self.assertEqual(ci_1['data']['real_size'], ns_1)
self.assertRaises(MockStorPoolExc,
libvirt_driver.extend_volume,
ci_2, mock.sentinel.instance)
ci_2, mock.sentinel.instance, rs_2)
libvirt_driver.connect_volume(ci_2, mock.sentinel.instance)
self.assertStorpoolAttached(('1', '2'))
ns_1 = libvirt_driver.extend_volume(ci_1, mock.sentinel.instance)
ns_1 = libvirt_driver.extend_volume(ci_1, mock.sentinel.instance, rs_1)
self.assertEqual(ci_1['data']['real_size'], ns_1)
ns_2 = libvirt_driver.extend_volume(ci_2, mock.sentinel.instance)
ns_2 = libvirt_driver.extend_volume(ci_2, mock.sentinel.instance, rs_2)
self.assertEqual(ci_2['data']['real_size'], ns_2)
self.assertRaises(MockStorPoolExc,
@ -168,7 +170,7 @@ class LibvirtStorPoolVolumeDriverTestCase(
self.assertRaises(MockStorPoolExc,
libvirt_driver.extend_volume,
ci_1, mock.sentinel.instance)
ci_1, mock.sentinel.instance, rs_1)
libvirt_driver.disconnect_volume(ci_2, mock.sentinel.instance)
self.assertDictEqual({}, test_attached)

View File

@ -587,7 +587,7 @@ class TestPowerVMDriver(test.NoDBTestCase):
@mock.patch('nova.virt.powervm.volume.fcvscsi.FCVscsiVolumeAdapter')
def test_extend_volume(self, mock_vscsi_adpt):
mock_bdm = self._fake_bdms()['block_device_mapping'][0]
self.drv.extend_volume(mock_bdm.get('connection_info'), self.inst)
self.drv.extend_volume(mock_bdm.get('connection_info'), self.inst, 0)
mock_vscsi_adpt.return_value.extend_volume.assert_called_once_with()
def test_vol_drv_iter(self):

View File

@ -522,13 +522,15 @@ class ComputeDriver(object):
"""
raise NotImplementedError()
def extend_volume(self, connection_info, instance):
def extend_volume(self, connection_info, instance, requested_size):
"""Extend the disk attached to the instance.
:param dict connection_info:
The connection for the extended volume.
:param nova.objects.instance.Instance instance:
The instance whose volume gets extended.
:param int requested_size
The requested new size of the volume in bytes
:return: None
"""

View File

@ -326,7 +326,7 @@ class FakeDriver(driver.ComputeDriver):
self._mounts[instance_name] = {}
self._mounts[instance_name][mountpoint] = new_connection_info
def extend_volume(self, connection_info, instance):
def extend_volume(self, connection_info, instance, requested_size):
"""Extend the disk attached to the instance."""
pass

View File

@ -1266,9 +1266,10 @@ class LibvirtDriver(driver.ComputeDriver):
driver_block_device.get_volume_id(connection_info),
instance=instance)
def _extend_volume(self, connection_info, instance):
def _extend_volume(self, connection_info, instance, requested_size):
vol_driver = self._get_volume_driver(connection_info)
return vol_driver.extend_volume(connection_info, instance)
return vol_driver.extend_volume(connection_info, instance,
requested_size)
def _use_native_luks(self, encryption=None):
"""Is LUKS the required provider and native QEMU LUKS available
@ -1618,9 +1619,10 @@ class LibvirtDriver(driver.ComputeDriver):
self._disconnect_volume(context, connection_info, instance,
encryption=encryption)
def extend_volume(self, connection_info, instance):
def extend_volume(self, connection_info, instance, requested_size):
try:
new_size = self._extend_volume(connection_info, instance)
new_size = self._extend_volume(connection_info, instance,
requested_size)
except NotImplementedError:
raise exception.ExtendVolumeNotSupported()
@ -1631,7 +1633,22 @@ class LibvirtDriver(driver.ComputeDriver):
state = guest.get_power_state(self._host)
active_state = state in (power_state.RUNNING, power_state.PAUSED)
if active_state:
disk_path = connection_info['data']['device_path']
if 'device_path' in connection_info['data']:
disk_path = connection_info['data']['device_path']
else:
# Some drivers (eg. net) don't put the device_path
# into the connection_info. Match disks by their serial
# number instead
volume_id = driver_block_device.get_volume_id(
connection_info)
disk = next(iter([
d for d in guest.get_all_disks()
if d.serial == volume_id
]), None)
if not disk:
raise exception.VolumeNotFound(volume_id=volume_id)
disk_path = disk.target_dev
LOG.debug('resizing block device %(dev)s to %(size)u kb',
{'dev': disk_path, 'size': new_size})
dev = guest.get_block_device(disk_path)

View File

@ -75,7 +75,7 @@ class LibvirtFibreChannelVolumeDriver(libvirt_volume.LibvirtBaseVolumeDriver):
super(LibvirtFibreChannelVolumeDriver,
self).disconnect_volume(connection_info, instance)
def extend_volume(self, connection_info, instance):
def extend_volume(self, connection_info, instance, requested_size):
"""Extend the volume."""
LOG.debug("calling os-brick to extend FC Volume", instance=instance)
new_size = self.connector.extend_volume(connection_info['data'])

View File

@ -80,7 +80,7 @@ class LibvirtISCSIVolumeDriver(libvirt_volume.LibvirtBaseVolumeDriver):
super(LibvirtISCSIVolumeDriver,
self).disconnect_volume(connection_info, instance)
def extend_volume(self, connection_info, instance):
def extend_volume(self, connection_info, instance, requested_size):
"""Extend the volume."""
LOG.debug("calling os-brick to extend iSCSI Volume", instance=instance)
new_size = self.connector.extend_volume(connection_info['data'])

View File

@ -132,3 +132,9 @@ class LibvirtNetVolumeDriver(libvirt_volume.LibvirtBaseVolumeDriver):
super(LibvirtNetVolumeDriver,
self).disconnect_volume(connection_info, instance)
self._delete_secret_by_name(connection_info)
def extend_volume(self, connection_info, instance, requested_size):
# There is nothing to do for network volumes. Cinder already extended
# the volume and there is no local block device which needs to be
# refreshed.
return requested_size

View File

@ -53,7 +53,7 @@ class LibvirtNVMEVolumeDriver(libvirt_volume.LibvirtVolumeDriver):
super(LibvirtNVMEVolumeDriver,
self).disconnect_volume(connection_info, instance)
def extend_volume(self, connection_info, instance):
def extend_volume(self, connection_info, instance, requested_size):
"""Extend the volume."""
LOG.debug("calling os-brick to extend NVMe Volume", instance=instance)
new_size = self.connector.extend_volume(connection_info['data'])

View File

@ -62,7 +62,7 @@ class LibvirtScaleIOVolumeDriver(libvirt_volume.LibvirtBaseVolumeDriver):
super(LibvirtScaleIOVolumeDriver, self).disconnect_volume(
connection_info, instance)
def extend_volume(self, connection_info, instance):
def extend_volume(self, connection_info, instance, requested_size):
LOG.debug("calling os-brick to extend ScaleIO Volume",
instance=instance)
new_size = self.connector.extend_volume(connection_info['data'])

View File

@ -46,7 +46,7 @@ class LibvirtStorPoolVolumeDriver(libvirt_volume.LibvirtVolumeDriver):
self.connector.disconnect_volume(connection_info['data'], None)
LOG.debug("Detached StorPool volume", instance=instance)
def extend_volume(self, connection_info, instance):
def extend_volume(self, connection_info, instance, requested_size):
"""Extend the volume."""
LOG.debug("Extending StorPool volume %s",
connection_info['data']['volume'], instance=instance)

View File

@ -132,8 +132,19 @@ class LibvirtBaseVolumeDriver(object):
"""Disconnect the volume."""
pass
def extend_volume(self, connection_info, instance):
"""Extend the volume."""
def extend_volume(self, connection_info, instance, requested_size):
"""Extend the volume.
:param: connection_info: connection information about the volume
that has been extended.
:param: instance: instance connected to the newly extended volume.
:param: requested_size: new extended size (in bytes) for the volume to
be extended.
:returns: the new size to use when resizing the disk in QEMU.
Note: the requested_size parameter is not used by all volume drivers
"""
raise NotImplementedError()

View File

@ -634,12 +634,13 @@ class PowerVMDriver(driver.ComputeDriver):
# Run the flow
tf_base.run(flow, instance=instance)
def extend_volume(self, connection_info, instance):
def extend_volume(self, connection_info, instance, requested_size):
"""Extend the disk attached to the instance.
:param dict connection_info: The connection for the extended volume.
:param nova.objects.instance.Instance instance:
The instance whose volume gets extended.
:param int requested_size: The requested new volume size in bytes.
:return: None
"""

View File

@ -0,0 +1,5 @@
---
features:
- |
Adds support for extending RBD attached volumes using the libvirt network
volume driver.