Enhance extend functionality

Cinderlib's volume extend functionality would extend the volume in the
backend yet ignore any refresh of the view of the volume if it was
attached.

It also didn't provide a way to refresh the view of an extended volume
given a Connection instance.

This patch adds the funtionality to automatically refresh the view when
extending a locally attached volume as well as to do the refresh using a
Connection instance.

The refresh is done making the appropriate extend_volume call to
os-brick's connector.

The OS-Brick RBD connector doesn't have the method because there's
nothing for it to do, but to provide a consistent API we add the method
to our NOS-Brick to return the new size like other connectors do.

Change-Id: I04a29597e7515526fb8505c76c3d7e3026b2d9c8
This commit is contained in:
Gorka Eguileor 2019-10-22 19:40:35 +02:00
parent 32ca96cb87
commit 6f989af09b
9 changed files with 162 additions and 6 deletions

View File

@ -148,6 +148,13 @@ class RBDConnector(connectors.rbd.RBDConnector):
return False
return True
def _get_vol_data(self, connection_properties):
self._setup_rbd_class()
pool, volume = connection_properties['name'].split('/')
link_name = self.get_rbd_device_name(pool, volume)
real_dev_path = os.path.realpath(link_name)
return link_name, real_dev_path
def _unmap(self, real_dev_path, conf_file, connection_properties):
if os.path.exists(real_dev_path):
cmd = ['rbd', 'unmap', real_dev_path, '--conf', conf_file]
@ -157,11 +164,8 @@ class RBDConnector(connectors.rbd.RBDConnector):
def disconnect_volume(self, connection_properties, device_info,
force=False, ignore_errors=False):
self._setup_rbd_class()
pool, volume = connection_properties['name'].split('/')
conf_file = device_info['conf']
link_name = self.get_rbd_device_name(pool, volume)
real_dev_path = os.path.realpath(link_name)
link_name, real_dev_path = self._get_vol_data(connection_properties)
self._unmap(real_dev_path, conf_file, connection_properties)
if self.containerized:
@ -193,6 +197,20 @@ class RBDConnector(connectors.rbd.RBDConnector):
# Don't check again to speed things on following connections
RBDConnector._setup_rbd_class = lambda *args: None
def extend_volume(self, connection_properties):
"""Refresh local volume view and return current size in bytes."""
# Nothing to do, RBD attached volumes are automatically refreshed, but
# we need to return the new size for compatibility
link_name, real_dev_path = self._get_vol_data(connection_properties)
device_name = os.path.basename(real_dev_path) # ie: rbd0
device_number = device_name[3:] # ie: 0
# Get size from /sys/devices/rbd/0/size instead of
# /sys/class/block/rbd0/size because the latter isn't updated
with open('/sys/devices/rbd/' + device_number + '/size') as f:
size_bytes = f.read().strip()
return int(size_bytes)
_setup_rbd_class = _setup_class

View File

@ -39,6 +39,7 @@ DEFAULT_PROJECT_ID = 'cinderlib'
DEFAULT_USER_ID = 'cinderlib'
BACKEND_NAME_SNAPSHOT_FIELD = 'progress'
CONNECTIONS_OVO_FIELD = 'volume_attachment'
GB = 1024 ** 3
# This cannot go in the setup method because cinderlib objects need them to
# be setup to set OVO_CLASS
@ -500,6 +501,11 @@ class Volume(NamedObject):
finally:
self.save()
if volume.status == 'in-use' and self.local_attach:
return self.local_attach.extend()
# Must return size in bytes
return size * GB
def clone(self, **new_vol_attrs):
new_vol_attrs['source_vol_id'] = self.id
new_vol = Volume(self, **new_vol_attrs)
@ -867,6 +873,9 @@ class Connection(Object, LazyVolumeAttr):
def save(self):
self.persistence.set_connection(self)
def extend(self):
return self.connector.extend_volume(self.conn_info['data'])
class Snapshot(NamedObject, LazyVolumeAttr):
OVO_CLASS = cinder_objs.Snapshot

View File

@ -179,6 +179,28 @@ class BackendFunctBasic(base_tests.BaseFunctTestCase):
result_new_size = self._get_vol_size(vol)
self.assertSize(new_size, result_new_size)
def test_extend_attached(self):
vol = self._create_vol(self.backend)
original_size = vol.size
# Attach, get size, and leave volume attached
result_original_size = self._get_vol_size(vol, do_detach=False)
self.assertSize(original_size, result_original_size)
new_size = vol.size + 1
# Extending the volume should also extend the local view of the volume
reported_size = vol.extend(new_size)
# The instance size must have been updated
self.assertEqual(new_size, vol.size)
self.assertEqual(new_size, vol._ovo.size)
# Returned size must match the requested one
self.assertEqual(new_size * (1024 ** 3), reported_size)
# Get size of attached volume on the host and detach it
result_new_size = self._get_vol_size(vol)
self.assertSize(new_size, result_new_size)
def test_clone(self):
vol = self._create_vol(self.backend)
original_size = self._get_vol_size(vol, do_detach=False)

View File

@ -282,3 +282,10 @@ class TestConnection(base.BaseTest):
def test_connected(self, value):
with mock.patch('cinderlib.objects.Connection.conn_info', value):
self.assertEqual(value, self.conn.connected)
def test_extend(self):
self.conn._ovo.connection_info['conn'] = {'data': mock.sentinel.data}
with mock.patch('cinderlib.objects.Connection.connector') as mock_conn:
res = self.conn.extend()
mock_conn.extend_volume.assert_called_once_with(mock.sentinel.data)
self.assertEqual(mock_conn.extend_volume.return_value, res)

View File

@ -173,13 +173,26 @@ class TestVolume(base.BaseTest):
def test_extend(self):
vol = objects.Volume(self.backend_name, status='available', size=10)
vol.extend(11)
res = vol.extend(11)
self.assertEqual(11 * (1024 ** 3), res) # size is in bytes not GBi
self.backend.driver.extend_volume.assert_called_once_with(vol._ovo, 11)
self.persistence.set_volume.assert_called_once_with(vol)
self.assertEqual('available', vol.status)
self.assertEqual(11, vol.size)
def test_extend_attached(self):
vol = objects.Volume(self.backend_name, status='in-use', size=10)
vol.local_attach = mock.Mock()
res = vol.extend(11)
self.assertEqual(vol.local_attach.extend.return_value, res)
self.backend.driver.extend_volume.assert_called_once_with(vol._ovo, 11)
vol.local_attach.extend.assert_called_once_with()
self.persistence.set_volume.assert_called_once_with(vol)
self.assertEqual('in-use', vol.status)
self.assertEqual(11, vol.size)
def test_extend_error(self):
vol = objects.Volume(self.backend_name, status='available', size=10)
self.backend.driver.extend_volume.side_effect = exception.NotFound

View File

@ -363,3 +363,19 @@ class TestRBDConnector(base.BaseTest):
exists_mock.assert_called_once_with(link)
remove_mock.assert_called_once_with(link)
link_mock.assert_called_once_with(source, link)
@mock.patch('six.moves.builtins.open')
@mock.patch.object(nos_brick.RBDConnector, '_get_vol_data')
def test_extend_volume(self, get_data_mock, open_mock):
get_data_mock.return_value = (
'/dev/rbd/rbd/volume-56539d26-2b78-49b8-8b96-160a62b0831f',
'/dev/rbd10')
cm_open = open_mock.return_value.__enter__.return_value
cm_open.read.return_value = '5368709120'
res = self.connector.extend_volume(mock.sentinel.connector_properties)
self.assertEqual(5 * (1024 ** 3), res) # 5 GBi
get_data_mock.assert_called_once_with(
mock.sentinel.connector_properties)
open_mock.assert_called_once_with('/sys/devices/rbd/10/size')

View File

@ -256,6 +256,61 @@ know when instantiating the driver by passing the
use_multipath_for_image_xfer=True,
)
Extend
------
The `Connection` object has an `extend` method that will refresh the host's
view of an attached volume to reflect the latest size of the volume and return
the new size in bytes.
There is no need to manually call this method for volumes that are locally
attached to the node that calls the `Volume`'s `extend` method, since that call
takes care of it.
When extending volumes that are attached to nodes other than the one calling
the `Volume`'s `extend` method we will need to either detach and re-attach the
volume on the host following the mechanisms explained above, or refresh the
current view of the volume.
How we refresh the host's view of an attached volume will depend on how we are
attaching the volumes.
With access to the metadata persistence storage
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
In this case things are easier, just like it was on the
`Remote connection`_.
Assuming we have a `volume_id` variable with the volume, and `storage` has the
`Backend` instance, all we need to do is:
.. code-block:: python
vol = storage.Volume.get_by_id(volume_id)
vol.connections[0].extend()
No access to the metadata persistence storage
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
This is more inconvenient, as you'll have to handle the data exchange manually
as well as the *OS-Brick* library calls to do the extend.
We'll need to get the connector information on the host that is going to do
the attach. Asuuming the dictionary is available in `connection_info` the
code would look like this:
.. code-block:: python
from os_brick.initiator import connector
connector_dict = connection_info['connector']
protocol = connection_info['conn']['driver_volume_type']
conn = connector.InitiatorConnector.factory(
protocol, 'sudo', user_multipath=True,
device_scan_attempts=3, conn=connector_dict)
conn.extend()
Multi attach
------------

View File

@ -224,16 +224,24 @@ The only parameter received by the `extend` method is the new size, and this
must always be greater than the current value because *cinderlib* is not
validating this at the moment.
The call will return the new size of the volume in bytes.
Example of creating, extending, and deleting a volume:
.. code-block:: python
vol = lvm.create_volume(size=1)
print('Vol %s has %s GBi' % (vol.id, vol.size))
vol.extend(2)
new_size = vol.extend(2)
print('Extended vol %s has %s GBi' % (vol.id, vol.size))
print('Detected new size is %s bytes' % new_size)
vol.delete()
A call to `extend` on a locally attached volume will automatically update the
host's view of the volume to reflect the new size. For non locally attached
volumes please refer to the `extend section in the connections
<connections.html#extend>`_ section.
Other methods
-------------

View File

@ -0,0 +1,8 @@
---
features:
- |
Enhance volume extend functionality in cinderlib by supporting the refresh
of the host's view of an attached volume that has been extended in the
backend to reflect the new size. A call to volume.extend will
automatically extend the view if the volume is locally attached and
connection.extend will do the same when run on a non controller host.