From 688c515b9d662486395d36c303ca599376a1dc0d Mon Sep 17 00:00:00 2001 From: Navneet Singh Date: Tue, 16 Jul 2013 10:43:55 +0530 Subject: [PATCH] NetApp NFS efficient clone_image impl This change implements clone_image funcionality for NFS drivers. It implements an internal image cache in nfs shares which will be used to clone images in case the image_location supplied does not represent an nfs share which could be used to directly clone image. It also implements an internal cache cleaning mechanism at regular interval. blueprint netapp-cinder-nfs-image-cloning Change-Id: I20eaddf9febc78df33e31bd41b4698b772da3ae7 --- cinder/image/glance.py | 27 +- cinder/tests/test_netapp_nfs.py | 521 ++++++++++++++++++++- cinder/tests/test_rbd.py | 9 +- cinder/volume/drivers/netapp/nfs.py | 586 +++++++++++++++++++++++- cinder/volume/drivers/netapp/options.py | 12 + cinder/volume/drivers/rbd.py | 1 + etc/cinder/cinder.conf.sample | 14 +- etc/cinder/rootwrap.d/volume.filters | 1 + 8 files changed, 1125 insertions(+), 46 deletions(-) diff --git a/cinder/image/glance.py b/cinder/image/glance.py index 5ff8c4da5..596fffcef 100644 --- a/cinder/image/glance.py +++ b/cinder/image/glance.py @@ -162,20 +162,18 @@ class GlanceClientWrapper(object): extra = "retrying" error_msg = _("Error contacting glance server " "'%(netloc)s' for '%(method)s', " - "%(extra)s.") % { - 'netloc': netloc, - 'method': method, - 'extra': extra, - } + "%(extra)s.") % {'netloc': netloc, + 'method': method, + 'extra': extra, + } if attempt == num_attempts: extra = 'done trying' error_msg = _("Error contacting glance server " "'%(netloc)s' for '%(method)s', " - "%(extra)s.") % { - 'netloc': netloc, - 'method': method, - 'extra': extra, - } + "%(extra)s.") % {'netloc': netloc, + 'method': method, + 'extra': extra, + } LOG.exception(error_msg) raise exception.GlanceConnectionFailed(netloc=netloc, reason=str(e)) @@ -237,7 +235,8 @@ class GlanceImageService(object): or None if this attribute is not shown by Glance. """ try: - client = GlanceClientWrapper() + # direct_url is returned by v2 api + client = GlanceClientWrapper(version=2) image_meta = client.call(context, 'get', image_id) except Exception: _reraise_translated_image_exception(image_id) @@ -245,7 +244,11 @@ class GlanceImageService(object): if not self._is_image_available(context, image_meta): raise exception.ImageNotFound(image_id=image_id) - return getattr(image_meta, 'direct_url', None) + # some glance stores like nfs only meta data + # is stored and returned as locations. + # so composite of two needs to be returned. + return (getattr(image_meta, 'direct_url', None), + getattr(image_meta, 'locations', None)) def download(self, context, image_id, data=None): """Calls out to Glance for data and writes data.""" diff --git a/cinder/tests/test_netapp_nfs.py b/cinder/tests/test_netapp_nfs.py index 31e7114b4..4bdf855d2 100644 --- a/cinder/tests/test_netapp_nfs.py +++ b/cinder/tests/test_netapp_nfs.py @@ -16,19 +16,24 @@ # under the License. """Unit tests for the NetApp-specific NFS driver module.""" +from lxml import etree +import mox +from mox import IgnoreArg +from mox import IsA +import os +import socket + from cinder import context from cinder import exception +from cinder.image import image_utils +from cinder.openstack.common import log as logging from cinder import test - from cinder.volume import configuration as conf from cinder.volume.drivers.netapp import api from cinder.volume.drivers.netapp import nfs as netapp_nfs -from lxml import etree -from mox import IgnoreArg -from mox import IsA -from mox import MockObject -import mox + +LOG = logging.getLogger(__name__) def create_configuration(): @@ -210,10 +215,12 @@ class NetappDirectCmodeNfsDriverTestCase(test.TestCase): mox = self.mox drv = self._driver + mox.StubOutWithMock(netapp_nfs.NetAppNFSDriver, 'do_setup') mox.StubOutWithMock(drv, 'check_for_setup_error') mox.StubOutWithMock(drv, '_get_client') mox.StubOutWithMock(drv, '_do_custom_setup') + netapp_nfs.NetAppNFSDriver.do_setup(IgnoreArg()) drv.check_for_setup_error() drv._get_client() drv._do_custom_setup(IgnoreArg()) @@ -290,6 +297,499 @@ class NetappDirectCmodeNfsDriverTestCase(test.TestCase): mox.VerifyAll() + def test_register_img_in_cache_noshare(self): + volume = {'id': '1', 'name': 'testvol'} + volume['provider_location'] = '10.61.170.1:/share/path' + drv = self._driver + mox = self.mox + mox.StubOutWithMock(drv, '_do_clone_rel_img_cache') + + drv._do_clone_rel_img_cache('testvol', 'img-cache-12345', + '10.61.170.1:/share/path', + 'img-cache-12345') + + mox.ReplayAll() + drv._register_image_in_cache(volume, '12345') + mox.VerifyAll() + + def test_register_img_in_cache_with_share(self): + volume = {'id': '1', 'name': 'testvol'} + volume['provider_location'] = '10.61.170.1:/share/path' + drv = self._driver + mox = self.mox + mox.StubOutWithMock(drv, '_do_clone_rel_img_cache') + + drv._do_clone_rel_img_cache('testvol', 'img-cache-12345', + '10.61.170.1:/share/path', + 'img-cache-12345') + + mox.ReplayAll() + drv._register_image_in_cache(volume, '12345') + mox.VerifyAll() + + def test_find_image_in_cache_no_shares(self): + drv = self._driver + drv._mounted_shares = [] + result = drv._find_image_in_cache('image_id') + if not result: + pass + else: + self.fail('Return result is unexpected') + + def test_find_image_in_cache_shares(self): + drv = self._driver + mox = self.mox + drv._mounted_shares = ['testshare'] + mox.StubOutWithMock(drv, '_get_mount_point_for_share') + mox.StubOutWithMock(os.path, 'exists') + + drv._get_mount_point_for_share('testshare').AndReturn('/mnt') + os.path.exists('/mnt/img-cache-id').AndReturn(True) + mox.ReplayAll() + result = drv._find_image_in_cache('id') + (share, file_name) = result[0] + mox.VerifyAll() + drv._mounted_shares.remove('testshare') + + if (share == 'testshare' and file_name == 'img-cache-id'): + pass + else: + LOG.warn(_("Share %(share)s and file name %(file_name)s") + % {'share': share, 'file_name': file_name}) + self.fail('Return result is unexpected') + + def test_find_old_cache_files_notexists(self): + drv = self._driver + mox = self.mox + cmd = ['find', '/mnt', '-maxdepth', '1', '-name', + 'img-cache*', '-amin', '+720'] + setattr(drv.configuration, 'expiry_thres_minutes', 720) + mox.StubOutWithMock(drv, '_get_mount_point_for_share') + mox.StubOutWithMock(drv, '_execute') + + drv._get_mount_point_for_share(IgnoreArg()).AndReturn('/mnt') + drv._execute(*cmd, run_as_root=True).AndReturn((None, '')) + mox.ReplayAll() + res = drv._find_old_cache_files('share') + mox.VerifyAll() + if len(res) == 0: + pass + else: + self.fail('No files expected but got return values.') + + def test_find_old_cache_files_exists(self): + drv = self._driver + mox = self.mox + cmd = ['find', '/mnt', '-maxdepth', '1', '-name', + 'img-cache*', '-amin', '+720'] + setattr(drv.configuration, 'expiry_thres_minutes', '720') + files = '/mnt/img-id1\n/mnt/img-id2\n' + r_files = ['img-id1', 'img-id2'] + mox.StubOutWithMock(drv, '_get_mount_point_for_share') + mox.StubOutWithMock(drv, '_execute') + mox.StubOutWithMock(drv, '_shortlist_del_eligible_files') + + drv._get_mount_point_for_share('share').AndReturn('/mnt') + drv._execute(*cmd, run_as_root=True).AndReturn((files, None)) + drv._shortlist_del_eligible_files( + IgnoreArg(), r_files).AndReturn(r_files) + mox.ReplayAll() + res = drv._find_old_cache_files('share') + mox.VerifyAll() + if len(res) == len(r_files): + for f in res: + r_files.remove(f) + else: + self.fail('Returned files not same as expected.') + + def test_delete_files_till_bytes_free_success(self): + drv = self._driver + mox = self.mox + files = [('img-cache-1', 230), ('img-cache-2', 380)] + mox.StubOutWithMock(drv, '_get_mount_point_for_share') + mox.StubOutWithMock(drv, '_delete_file') + + drv._get_mount_point_for_share(IgnoreArg()).AndReturn('/mnt') + drv._delete_file('/mnt/img-cache-2').AndReturn(True) + drv._delete_file('/mnt/img-cache-1').AndReturn(True) + mox.ReplayAll() + drv._delete_files_till_bytes_free(files, 'share', bytes_to_free=1024) + mox.VerifyAll() + + def test_clean_image_cache_exec(self): + drv = self._driver + mox = self.mox + drv.configuration.thres_avl_size_perc_start = 20 + drv.configuration.thres_avl_size_perc_stop = 50 + drv._mounted_shares = ['testshare'] + + mox.StubOutWithMock(drv, '_find_old_cache_files') + mox.StubOutWithMock(drv, '_delete_files_till_bytes_free') + mox.StubOutWithMock(drv, '_get_capacity_info') + + drv._get_capacity_info('testshare').AndReturn((100, 19, 81)) + drv._find_old_cache_files('testshare').AndReturn(['f1', 'f2']) + drv._delete_files_till_bytes_free( + ['f1', 'f2'], 'testshare', bytes_to_free=31) + mox.ReplayAll() + drv._clean_image_cache() + mox.VerifyAll() + drv._mounted_shares.remove('testshare') + if not drv.cleaning: + pass + else: + self.fail('Clean image cache failed.') + + def test_clean_image_cache_noexec(self): + drv = self._driver + mox = self.mox + drv.configuration.thres_avl_size_perc_start = 20 + drv.configuration.thres_avl_size_perc_stop = 50 + drv._mounted_shares = ['testshare'] + + mox.StubOutWithMock(drv, '_get_capacity_info') + + drv._get_capacity_info('testshare').AndReturn((100, 30, 70)) + mox.ReplayAll() + drv._clean_image_cache() + mox.VerifyAll() + drv._mounted_shares.remove('testshare') + if not drv.cleaning: + pass + else: + self.fail('Clean image cache failed.') + + def test_clone_image_fromcache(self): + drv = self._driver + mox = self.mox + volume = {'name': 'vol', 'size': '20'} + mox.StubOutWithMock(drv, '_find_image_in_cache') + mox.StubOutWithMock(drv, '_do_clone_rel_img_cache') + mox.StubOutWithMock(drv, '_post_clone_image') + mox.StubOutWithMock(drv, '_is_share_eligible') + + drv._find_image_in_cache(IgnoreArg()).AndReturn( + [('share', 'file_name')]) + drv._is_share_eligible(IgnoreArg(), IgnoreArg()).AndReturn(True) + drv._do_clone_rel_img_cache('file_name', 'vol', 'share', 'file_name') + drv._post_clone_image(volume) + + mox.ReplayAll() + drv. clone_image(volume, ('image_location', None), 'image_id') + mox.VerifyAll() + + def get_img_info(self, format): + class img_info(object): + def __init__(self, fmt): + self.file_format = fmt + + return img_info(format) + + def test_clone_image_cloneableshare_nospace(self): + drv = self._driver + mox = self.mox + volume = {'name': 'vol', 'size': '20'} + mox.StubOutWithMock(drv, '_find_image_in_cache') + mox.StubOutWithMock(drv, '_is_cloneable_share') + mox.StubOutWithMock(drv, '_is_share_eligible') + + drv._find_image_in_cache(IgnoreArg()).AndReturn([]) + drv._is_cloneable_share(IgnoreArg()).AndReturn('127.0.0.1:/share') + drv._is_share_eligible(IgnoreArg(), IgnoreArg()).AndReturn(False) + + mox.ReplayAll() + (prop, cloned) = drv. clone_image( + volume, ('nfs://127.0.0.1:/share/img-id', None), 'image_id') + mox.VerifyAll() + if not cloned and not prop['provider_location']: + pass + else: + self.fail('Expected not cloned, got cloned.') + + def test_clone_image_cloneableshare_raw(self): + drv = self._driver + mox = self.mox + volume = {'name': 'vol', 'size': '20'} + mox.StubOutWithMock(drv, '_find_image_in_cache') + mox.StubOutWithMock(drv, '_is_cloneable_share') + mox.StubOutWithMock(drv, '_get_mount_point_for_share') + mox.StubOutWithMock(image_utils, 'qemu_img_info') + mox.StubOutWithMock(drv, '_clone_volume') + mox.StubOutWithMock(drv, '_discover_file_till_timeout') + mox.StubOutWithMock(drv, '_set_rw_permissions_for_all') + mox.StubOutWithMock(drv, '_resize_image_file') + mox.StubOutWithMock(drv, '_is_share_eligible') + + drv._find_image_in_cache(IgnoreArg()).AndReturn([]) + drv._is_cloneable_share(IgnoreArg()).AndReturn('127.0.0.1:/share') + drv._is_share_eligible(IgnoreArg(), IgnoreArg()).AndReturn(True) + drv._get_mount_point_for_share(IgnoreArg()).AndReturn('/mnt') + image_utils.qemu_img_info('/mnt/img-id').AndReturn( + self.get_img_info('raw')) + drv._clone_volume( + 'img-id', 'vol', share='127.0.0.1:/share', volume_id=None) + drv._get_mount_point_for_share(IgnoreArg()).AndReturn('/mnt') + drv._discover_file_till_timeout(IgnoreArg()).AndReturn(True) + drv._set_rw_permissions_for_all('/mnt/vol') + drv._resize_image_file({'name': 'vol'}, IgnoreArg()) + + mox.ReplayAll() + drv. clone_image( + volume, ('nfs://127.0.0.1:/share/img-id', None), 'image_id') + mox.VerifyAll() + + def test_clone_image_cloneableshare_notraw(self): + drv = self._driver + mox = self.mox + volume = {'name': 'vol', 'size': '20'} + mox.StubOutWithMock(drv, '_find_image_in_cache') + mox.StubOutWithMock(drv, '_is_cloneable_share') + mox.StubOutWithMock(drv, '_get_mount_point_for_share') + mox.StubOutWithMock(image_utils, 'qemu_img_info') + mox.StubOutWithMock(drv, '_clone_volume') + mox.StubOutWithMock(drv, '_discover_file_till_timeout') + mox.StubOutWithMock(drv, '_set_rw_permissions_for_all') + mox.StubOutWithMock(drv, '_resize_image_file') + mox.StubOutWithMock(image_utils, 'convert_image') + mox.StubOutWithMock(drv, '_register_image_in_cache') + mox.StubOutWithMock(drv, '_is_share_eligible') + + drv._find_image_in_cache(IgnoreArg()).AndReturn([]) + drv._is_cloneable_share('nfs://127.0.0.1/share/img-id').AndReturn( + '127.0.0.1:/share') + drv._is_share_eligible(IgnoreArg(), IgnoreArg()).AndReturn(True) + drv._get_mount_point_for_share('127.0.0.1:/share').AndReturn('/mnt') + image_utils.qemu_img_info('/mnt/img-id').AndReturn( + self.get_img_info('notraw')) + image_utils.convert_image(IgnoreArg(), IgnoreArg(), 'raw') + image_utils.qemu_img_info('/mnt/vol').AndReturn( + self.get_img_info('raw')) + drv._register_image_in_cache(IgnoreArg(), IgnoreArg()) + drv._get_mount_point_for_share('127.0.0.1:/share').AndReturn('/mnt') + drv._discover_file_till_timeout(IgnoreArg()).AndReturn(True) + drv._set_rw_permissions_for_all('/mnt/vol') + drv._resize_image_file({'name': 'vol'}, IgnoreArg()) + + mox.ReplayAll() + drv. clone_image( + volume, ('nfs://127.0.0.1/share/img-id', None), 'image_id') + mox.VerifyAll() + + def test_clone_image_file_not_discovered(self): + drv = self._driver + mox = self.mox + volume = {'name': 'vol', 'size': '20'} + mox.StubOutWithMock(drv, '_find_image_in_cache') + mox.StubOutWithMock(drv, '_is_cloneable_share') + mox.StubOutWithMock(drv, '_get_mount_point_for_share') + mox.StubOutWithMock(image_utils, 'qemu_img_info') + mox.StubOutWithMock(drv, '_clone_volume') + mox.StubOutWithMock(drv, '_discover_file_till_timeout') + mox.StubOutWithMock(image_utils, 'convert_image') + mox.StubOutWithMock(drv, '_register_image_in_cache') + mox.StubOutWithMock(drv, '_is_share_eligible') + mox.StubOutWithMock(drv, 'local_path') + mox.StubOutWithMock(os.path, 'exists') + mox.StubOutWithMock(drv, '_delete_file') + + drv._find_image_in_cache(IgnoreArg()).AndReturn([]) + drv._is_cloneable_share('nfs://127.0.0.1/share/img-id').AndReturn( + '127.0.0.1:/share') + drv._is_share_eligible(IgnoreArg(), IgnoreArg()).AndReturn(True) + drv._get_mount_point_for_share('127.0.0.1:/share').AndReturn('/mnt') + image_utils.qemu_img_info('/mnt/img-id').AndReturn( + self.get_img_info('notraw')) + image_utils.convert_image(IgnoreArg(), IgnoreArg(), 'raw') + image_utils.qemu_img_info('/mnt/vol').AndReturn( + self.get_img_info('raw')) + drv._register_image_in_cache(IgnoreArg(), IgnoreArg()) + drv.local_path(IgnoreArg()).AndReturn('/mnt/vol') + drv._discover_file_till_timeout(IgnoreArg()).AndReturn(False) + drv.local_path(IgnoreArg()).AndReturn('/mnt/vol') + os.path.exists('/mnt/vol').AndReturn(True) + drv._delete_file('/mnt/vol') + + mox.ReplayAll() + vol_dict, result = drv. clone_image( + volume, ('nfs://127.0.0.1/share/img-id', None), 'image_id') + mox.VerifyAll() + self.assertFalse(result) + self.assertFalse(vol_dict['bootable']) + self.assertEqual(vol_dict['provider_location'], None) + + def test_clone_image_resizefails(self): + drv = self._driver + mox = self.mox + volume = {'name': 'vol', 'size': '20'} + mox.StubOutWithMock(drv, '_find_image_in_cache') + mox.StubOutWithMock(drv, '_is_cloneable_share') + mox.StubOutWithMock(drv, '_get_mount_point_for_share') + mox.StubOutWithMock(image_utils, 'qemu_img_info') + mox.StubOutWithMock(drv, '_clone_volume') + mox.StubOutWithMock(drv, '_discover_file_till_timeout') + mox.StubOutWithMock(drv, '_set_rw_permissions_for_all') + mox.StubOutWithMock(drv, '_resize_image_file') + mox.StubOutWithMock(image_utils, 'convert_image') + mox.StubOutWithMock(drv, '_register_image_in_cache') + mox.StubOutWithMock(drv, '_is_share_eligible') + mox.StubOutWithMock(drv, 'local_path') + mox.StubOutWithMock(os.path, 'exists') + mox.StubOutWithMock(drv, '_delete_file') + + drv._find_image_in_cache(IgnoreArg()).AndReturn([]) + drv._is_cloneable_share('nfs://127.0.0.1/share/img-id').AndReturn( + '127.0.0.1:/share') + drv._is_share_eligible(IgnoreArg(), IgnoreArg()).AndReturn(True) + drv._get_mount_point_for_share('127.0.0.1:/share').AndReturn('/mnt') + image_utils.qemu_img_info('/mnt/img-id').AndReturn( + self.get_img_info('notraw')) + image_utils.convert_image(IgnoreArg(), IgnoreArg(), 'raw') + image_utils.qemu_img_info('/mnt/vol').AndReturn( + self.get_img_info('raw')) + drv._register_image_in_cache(IgnoreArg(), IgnoreArg()) + drv.local_path(IgnoreArg()).AndReturn('/mnt/vol') + drv._discover_file_till_timeout(IgnoreArg()).AndReturn(True) + drv._set_rw_permissions_for_all('/mnt/vol') + drv._resize_image_file( + IgnoreArg(), IgnoreArg()).AndRaise(exception.InvalidResults()) + drv.local_path(IgnoreArg()).AndReturn('/mnt/vol') + os.path.exists('/mnt/vol').AndReturn(True) + drv._delete_file('/mnt/vol') + + mox.ReplayAll() + vol_dict, result = drv. clone_image( + volume, ('nfs://127.0.0.1/share/img-id', None), 'image_id') + mox.VerifyAll() + self.assertFalse(result) + self.assertFalse(vol_dict['bootable']) + self.assertEqual(vol_dict['provider_location'], None) + + def test_is_cloneable_share_badformats(self): + drv = self._driver + strgs = ['10.61.666.22:/share/img', + 'nfs://10.61.666.22:/share/img', + 'nfs://10.61.666.22//share/img', + 'nfs://com.netapp.com:/share/img', + 'nfs://com.netapp.com//share/img', + 'com.netapp.com://share/im\g', + 'http://com.netapp.com://share/img', + 'nfs://com.netapp.com:/share/img', + 'nfs://com.netapp.com:8080//share/img' + 'nfs://com.netapp.com//img', + 'nfs://[ae::sr::ty::po]/img'] + for strg in strgs: + res = drv._is_cloneable_share(strg) + if res: + msg = 'Invalid format matched for url %s.' % strg + self.fail(msg) + + def test_is_cloneable_share_goodformat1(self): + drv = self._driver + mox = self.mox + strg = 'nfs://10.61.222.333/share/img' + mox.StubOutWithMock(drv, '_check_share_in_use') + drv._check_share_in_use(IgnoreArg(), IgnoreArg()).AndReturn('share') + mox.ReplayAll() + drv._is_cloneable_share(strg) + mox.VerifyAll() + + def test_is_cloneable_share_goodformat2(self): + drv = self._driver + mox = self.mox + strg = 'nfs://10.61.222.333:8080/share/img' + mox.StubOutWithMock(drv, '_check_share_in_use') + drv._check_share_in_use(IgnoreArg(), IgnoreArg()).AndReturn('share') + mox.ReplayAll() + drv._is_cloneable_share(strg) + mox.VerifyAll() + + def test_is_cloneable_share_goodformat3(self): + drv = self._driver + mox = self.mox + strg = 'nfs://com.netapp:8080/share/img' + mox.StubOutWithMock(drv, '_check_share_in_use') + drv._check_share_in_use(IgnoreArg(), IgnoreArg()).AndReturn('share') + mox.ReplayAll() + drv._is_cloneable_share(strg) + mox.VerifyAll() + + def test_is_cloneable_share_goodformat4(self): + drv = self._driver + mox = self.mox + strg = 'nfs://netapp.com/share/img' + mox.StubOutWithMock(drv, '_check_share_in_use') + drv._check_share_in_use(IgnoreArg(), IgnoreArg()).AndReturn('share') + mox.ReplayAll() + drv._is_cloneable_share(strg) + mox.VerifyAll() + + def test_is_cloneable_share_goodformat5(self): + drv = self._driver + mox = self.mox + strg = 'nfs://netapp.com/img' + mox.StubOutWithMock(drv, '_check_share_in_use') + drv._check_share_in_use(IgnoreArg(), IgnoreArg()).AndReturn('share') + mox.ReplayAll() + drv._is_cloneable_share(strg) + mox.VerifyAll() + + def test_check_share_in_use_no_conn(self): + drv = self._driver + share = drv._check_share_in_use(None, '/dir') + if share: + self.fail('Unexpected share detected.') + + def test_check_share_in_use_invalid_conn(self): + drv = self._driver + share = drv._check_share_in_use(':8989', '/dir') + if share: + self.fail('Unexpected share detected.') + + def test_check_share_in_use_incorrect_host(self): + drv = self._driver + mox = self.mox + mox.StubOutWithMock(socket, 'gethostbyname') + socket.gethostbyname(IgnoreArg()).AndRaise(Exception()) + mox.ReplayAll() + share = drv._check_share_in_use('incorrect:8989', '/dir') + mox.VerifyAll() + if share: + self.fail('Unexpected share detected.') + + def test_check_share_in_use_success(self): + drv = self._driver + mox = self.mox + drv._mounted_shares = ['127.0.0.1:/dir/share'] + mox.StubOutWithMock(socket, 'gethostbyname') + mox.StubOutWithMock(drv, '_share_match_for_ip') + socket.gethostbyname(IgnoreArg()).AndReturn('10.22.33.44') + drv._share_match_for_ip( + '10.22.33.44', ['127.0.0.1:/dir/share']).AndReturn('share') + mox.ReplayAll() + share = drv._check_share_in_use('127.0.0.1:8989', '/dir/share') + mox.VerifyAll() + if not share: + self.fail('Expected share not detected') + + def test_construct_image_url_loc(self): + drv = self._driver + img_loc = (None, + [{'metadata': + {'share_location': 'nfs://host/path', + 'mount_point': '/opt/stack/data/glance', + 'type': 'nfs'}, + 'url': 'file:///opt/stack/data/glance/image-id'}]) + location = drv._construct_image_nfs_url(img_loc) + if location != "nfs://host/path/image-id": + self.fail("Unexpected direct url.") + + def test_construct_image_url_direct(self): + drv = self._driver + img_loc = ("nfs://host/path/image-id", None) + location = drv._construct_image_nfs_url(img_loc) + if location != "nfs://host/path/image-id": + self.fail("Unexpected direct url.") + class NetappDirect7modeNfsDriverTestCase(NetappDirectCmodeNfsDriverTestCase): """Test direct NetApp C Mode driver.""" @@ -331,11 +831,11 @@ class NetappDirect7modeNfsDriverTestCase(NetappDirectCmodeNfsDriverTestCase): def test_do_setup(self): mox = self.mox drv = self._driver - + mox.StubOutWithMock(netapp_nfs.NetAppNFSDriver, 'do_setup') mox.StubOutWithMock(drv, 'check_for_setup_error') mox.StubOutWithMock(drv, '_get_client') mox.StubOutWithMock(drv, '_do_custom_setup') - + netapp_nfs.NetAppNFSDriver.do_setup(IgnoreArg()) drv.check_for_setup_error() drv._get_client() drv._do_custom_setup(IgnoreArg()) @@ -353,14 +853,15 @@ class NetappDirect7modeNfsDriverTestCase(NetappDirectCmodeNfsDriverTestCase): volume = FakeVolume() setattr(volume, 'provider_location', '127.0.0.1:/nfs') - mox.StubOutWithMock(drv, '_get_export_path') + mox.StubOutWithMock(drv, '_get_export_ip_path') mox.StubOutWithMock(drv, '_get_actual_path_for_export') mox.StubOutWithMock(drv, '_start_clone') mox.StubOutWithMock(drv, '_wait_for_clone_finish') if status == 'fail': mox.StubOutWithMock(drv, '_clear_clone') - drv._get_export_path(IgnoreArg()).AndReturn('/nfs') + drv._get_export_ip_path( + IgnoreArg(), IgnoreArg()).AndReturn(('127.0.0.1', '/nfs')) drv._get_actual_path_for_export(IgnoreArg()).AndReturn('/vol/vol1/nfs') drv._start_clone(IgnoreArg(), IgnoreArg()).AndReturn(('1', '2')) if status == 'fail': diff --git a/cinder/tests/test_rbd.py b/cinder/tests/test_rbd.py index 843dbc855..d782fdc73 100644 --- a/cinder/tests/test_rbd.py +++ b/cinder/tests/test_rbd.py @@ -558,8 +558,9 @@ class ManagedRBDTestCase(DriverTestCase): expected = ({}, False) self.stubs.Set(self.volume.driver, '_is_cloneable', lambda x: False) - actual = self.volume.driver.clone_image(object(), object(), object()) - self.assertEqual(expected, actual) + image_loc = (object(), object()) + actual = self.volume.driver.clone_image(object(), image_loc, object()) + self.assertEquals(expected, actual) self.stubs.Set(self.volume.driver, '_is_cloneable', lambda x: True) self.assertEqual(expected, @@ -573,8 +574,8 @@ class ManagedRBDTestCase(DriverTestCase): self.stubs.Set(self.volume.driver, '_clone', lambda *args: None) self.stubs.Set(self.volume.driver, '_resize', lambda *args: None) - actual = self.volume.driver.clone_image(object(), object(), object()) - self.assertEqual(expected, actual) + actual = self.volume.driver.clone_image(object(), image_loc, object()) + self.assertEquals(expected, actual) def test_clone_success(self): self.stubs.Set(self.volume.driver, '_is_cloneable', lambda x: True) diff --git a/cinder/volume/drivers/netapp/nfs.py b/cinder/volume/drivers/netapp/nfs.py index 477a4c11e..e5599a3fe 100644 --- a/cinder/volume/drivers/netapp/nfs.py +++ b/cinder/volume/drivers/netapp/nfs.py @@ -20,11 +20,16 @@ Volume driver for NetApp NFS storage. import copy import os +import re +import socket +from threading import Timer import time +import urlparse from oslo.config import cfg from cinder import exception +from cinder.image import image_utils from cinder.openstack.common import log as logging from cinder.openstack.common import processutils from cinder import units @@ -35,8 +40,10 @@ from cinder.volume.drivers.netapp.api import NaServer from cinder.volume.drivers.netapp.options import netapp_basicauth_opts from cinder.volume.drivers.netapp.options import netapp_cluster_opts from cinder.volume.drivers.netapp.options import netapp_connection_opts +from cinder.volume.drivers.netapp.options import netapp_img_cache_opts from cinder.volume.drivers.netapp.options import netapp_transport_opts from cinder.volume.drivers.netapp import ssc_utils +from cinder.volume.drivers.netapp import utils as na_utils from cinder.volume.drivers.netapp.utils import get_volume_extra_specs from cinder.volume.drivers.netapp.utils import provide_ems from cinder.volume.drivers.netapp.utils import validate_instantiation @@ -50,6 +57,7 @@ CONF = cfg.CONF CONF.register_opts(netapp_connection_opts) CONF.register_opts(netapp_transport_opts) CONF.register_opts(netapp_basicauth_opts) +CONF.register_opts(netapp_img_cache_opts) class NetAppNFSDriver(nfs.NfsDriver): @@ -68,12 +76,13 @@ class NetAppNFSDriver(nfs.NfsDriver): self.configuration.append_config_values(netapp_connection_opts) self.configuration.append_config_values(netapp_basicauth_opts) self.configuration.append_config_values(netapp_transport_opts) + self.configuration.append_config_values(netapp_img_cache_opts) def set_execute(self, execute): self._execute = execute def do_setup(self, context): - raise NotImplementedError() + super(NetAppNFSDriver, self).do_setup(context) def check_for_setup_error(self): """Returns an error if prerequisites aren't met.""" @@ -121,8 +130,8 @@ class NetAppNFSDriver(nfs.NfsDriver): export_path = self._get_export_path(volume_id) return (nfs_server_ip + ':' + export_path) - def _clone_volume(self, volume_name, clone_name, volume_id): - """Clones mounted volume with OnCommand proxy API.""" + def _clone_volume(self, volume_name, clone_name, volume_id, share=None): + """Clones mounted volume using NetApp api.""" raise NotImplementedError() def _get_provider_location(self, volume_id): @@ -196,6 +205,384 @@ class NetAppNFSDriver(nfs.NfsDriver): def _update_volume_stats(self): """Retrieve stats info from volume group.""" super(NetAppNFSDriver, self)._update_volume_stats() + self._spawn_clean_cache_job() + + def copy_image_to_volume(self, context, volume, image_service, image_id): + """Fetch the image from image_service and write it to the volume.""" + super(NetAppNFSDriver, self).copy_image_to_volume( + context, volume, image_service, image_id) + LOG.info(_('Copied image to volume %s'), volume['name']) + self._register_image_in_cache(volume, image_id) + + def _register_image_in_cache(self, volume, image_id): + """Stores image in the cache.""" + file_name = 'img-cache-%s' % image_id + LOG.info(_("Registering image in cache %s"), file_name) + try: + self._do_clone_rel_img_cache( + volume['name'], file_name, + volume['provider_location'], file_name) + except Exception as e: + LOG.warn( + _('Exception while registering image %(image_id)s' + ' in cache. Exception: %(exc)s') + % {'image_id': image_id, 'exc': e.__str__()}) + + def _find_image_in_cache(self, image_id): + """Finds image in cache and returns list of shares with file name.""" + result = [] + if getattr(self, '_mounted_shares', None): + for share in self._mounted_shares: + dir = self._get_mount_point_for_share(share) + file_name = 'img-cache-%s' % image_id + file_path = '%s/%s' % (dir, file_name) + if os.path.exists(file_path): + LOG.debug(_('Found cache file for image %(image_id)s' + ' on share %(share)s') + % {'image_id': image_id, 'share': share}) + result.append((share, file_name)) + return result + + def _do_clone_rel_img_cache(self, src, dst, share, cache_file): + """Do clone operation w.r.t image cache file.""" + @utils.synchronized(cache_file, external=True) + def _do_clone(): + dir = self._get_mount_point_for_share(share) + file_path = '%s/%s' % (dir, dst) + if not os.path.exists(file_path): + LOG.info(_('Cloning img from cache for %s'), dst) + self._clone_volume(src, dst, volume_id=None, share=share) + _do_clone() + + @utils.synchronized('clean_cache') + def _spawn_clean_cache_job(self): + """Spawns a clean task if not running.""" + if getattr(self, 'cleaning', None): + LOG.debug(_('Image cache cleaning in progress. Returning... ')) + return + else: + #set cleaning to True + self.cleaning = True + t = Timer(0, self._clean_image_cache) + t.start() + + def _clean_image_cache(self): + """Clean the image cache files in cache of space crunch.""" + try: + LOG.debug(_('Image cache cleaning in progress.')) + thres_size_perc_start =\ + self.configuration.thres_avl_size_perc_start + thres_size_perc_stop =\ + self.configuration.thres_avl_size_perc_stop + for share in getattr(self, '_mounted_shares', []): + try: + total_size, total_avl, total_alc =\ + self._get_capacity_info(share) + avl_percent = int((total_avl / total_size) * 100) + if avl_percent <= thres_size_perc_start: + LOG.info(_('Cleaning cache for share %s.'), share) + eligible_files = self._find_old_cache_files(share) + threshold_size = int( + (thres_size_perc_stop * total_size) / 100) + bytes_to_free = int(threshold_size - total_avl) + LOG.debug(_('Files to be queued for deletion %s'), + eligible_files) + self._delete_files_till_bytes_free( + eligible_files, share, bytes_to_free) + else: + continue + except Exception as e: + LOG.warn(_( + 'Exception during cache cleaning' + ' %(share)s. Message - %(ex)s') + % {'share': share, 'ex': e.__str__()}) + continue + finally: + LOG.debug(_('Image cache cleaning done.')) + self.cleaning = False + + def _shortlist_del_eligible_files(self, share, old_files): + """Prepares list of eligible files to be deleted from cache.""" + raise NotImplementedError() + + def _find_old_cache_files(self, share): + """Finds the old files in cache.""" + mount_fs = self._get_mount_point_for_share(share) + threshold_minutes = self.configuration.expiry_thres_minutes + cmd = ['find', mount_fs, '-maxdepth', '1', '-name', + 'img-cache*', '-amin', '+%s' % (threshold_minutes)] + res, __ = self._execute(*cmd, run_as_root=True) + if res: + old_file_paths = res.strip('\n').split('\n') + mount_fs_len = len(mount_fs) + old_files = [x[mount_fs_len + 1:] for x in old_file_paths] + eligible_files = self._shortlist_del_eligible_files( + share, old_files) + return eligible_files + return [] + + def _delete_files_till_bytes_free(self, file_list, share, bytes_to_free=0): + """Delete files from disk till bytes are freed or list exhausted.""" + LOG.debug(_('Bytes to free %s'), bytes_to_free) + if file_list and bytes_to_free > 0: + sorted_files = sorted(file_list, key=lambda x: x[1], reverse=True) + mount_fs = self._get_mount_point_for_share(share) + for f in sorted_files: + if f: + file_path = '%s/%s' % (mount_fs, f[0]) + LOG.debug(_('Delete file path %s'), file_path) + + @utils.synchronized(f[0], external=True) + def _do_delete(): + if self._delete_file(file_path): + return True + return False + if _do_delete(): + bytes_to_free = bytes_to_free - int(f[1]) + if bytes_to_free <= 0: + return + + def _delete_file(self, path): + """Delete file from disk and return result as boolean.""" + try: + LOG.debug(_('Deleting file at path %s'), path) + cmd = ['rm', '-f', path] + self._execute(*cmd, run_as_root=True) + return True + except Exception as ex: + LOG.warning(_('Exception during deleting %s'), ex.__str__()) + return False + + def clone_image(self, volume, image_location, image_id): + """Create a volume efficiently from an existing image. + + image_location is a string whose format depends on the + image service backend in use. The driver should use it + to determine whether cloning is possible. + + image_id is a string which represents id of the image. + It can be used by the driver to introspect internal + stores or registry to do an efficient image clone. + + Returns a dict of volume properties eg. provider_location, + boolean indicating whether cloning occurred. + """ + + cloned = False + post_clone = False + share = None + try: + cache_result = self._find_image_in_cache(image_id) + if cache_result: + cloned = self._clone_from_cache(volume, image_id, cache_result) + else: + cloned = self._direct_nfs_clone(volume, image_location, + image_id) + if cloned: + post_clone = self._post_clone_image(volume) + except Exception as e: + msg = e.msg if getattr(e, 'msg', None) else e.__str__() + LOG.warn(_('Unexpected exception in cloning image' + ' %(image_id)s. Message: %(msg)s') + % {'image_id': image_id, 'msg': msg}) + vol_path = self.local_path(volume) + volume['provider_location'] = None + if os.path.exists(vol_path): + self._delete_file(vol_path) + finally: + cloned = cloned and post_clone + share = volume['provider_location'] if cloned else None + bootable = True if cloned else False + return {'provider_location': share, 'bootable': bootable}, cloned + + def _clone_from_cache(self, volume, image_id, cache_result): + """Clones a copy from image cache.""" + cloned = False + LOG.info(_('Cloning image %s from cache'), image_id) + for res in cache_result: + # Repeat tries in other shares if failed in some + (share, file_name) = res + LOG.debug(_('Cache share: %s'), share) + if (share and + self._is_share_eligible(share, volume['size'])): + try: + self._do_clone_rel_img_cache( + file_name, volume['name'], share, file_name) + cloned = True + volume['provider_location'] = share + break + except Exception: + LOG.warn(_('Unexpected exception during' + ' image cloning in share %s'), share) + return cloned + + def _direct_nfs_clone(self, volume, image_location, image_id): + """Clone directly in nfs share.""" + LOG.info(_('Cloning image %s directly in share'), image_id) + cloned = False + image_location = self._construct_image_nfs_url(image_location) + share = self._is_cloneable_share(image_location) + if share and self._is_share_eligible(share, volume['size']): + LOG.debug(_('Share is cloneable %s'), share) + volume['provider_location'] = share + (__, ___, img_file) = image_location.rpartition('/') + dir_path = self._get_mount_point_for_share(share) + img_path = '%s/%s' % (dir_path, img_file) + img_info = image_utils.qemu_img_info(img_path) + if img_info.file_format == 'raw': + LOG.debug(_('Image is raw %s'), image_id) + self._clone_volume( + img_file, volume['name'], + volume_id=None, share=share) + cloned = True + else: + LOG.info( + _('Image will locally be converted to raw %s'), + image_id) + dst = '%s/%s' % (dir_path, volume['name']) + image_utils.convert_image(img_path, dst, 'raw') + data = image_utils.qemu_img_info(dst) + if data.file_format != "raw": + raise exception.InvalidResults( + _("Converted to raw, but" + " format is now %s") % data.file_format) + else: + cloned = True + self._register_image_in_cache( + volume, image_id) + return cloned + + def _post_clone_image(self, volume): + """Do operations post image cloning.""" + LOG.info(_('Performing post clone for %s'), volume['name']) + vol_path = self.local_path(volume) + if self._discover_file_till_timeout(vol_path): + self._set_rw_permissions_for_all(vol_path) + self._resize_image_file(vol_path, volume['size']) + return True + raise exception.InvalidResults( + _("NFS file could not be discovered.")) + + def _resize_image_file(self, path, new_size): + """Resize the image file on share to new size.""" + LOG.debug(_('Checking file for resize')) + if self._is_file_size_equal(path, new_size): + return + else: + LOG.info(_('Resizing file to %sG'), new_size) + image_utils.resize_image(path, new_size) + if self._is_file_size_equal(path, new_size): + return + else: + raise exception.InvalidResults( + _('Resizing image file failed.')) + + def _is_file_size_equal(self, path, size): + """Checks if file size at path is equal to size.""" + data = image_utils.qemu_img_info(path) + virt_size = data.virtual_size / units.GiB + if virt_size == size: + return True + else: + return False + + def _discover_file_till_timeout(self, path, timeout=45): + """Checks if file size at path is equal to size.""" + # Sometimes nfs takes time to discover file + # Retrying in case any unexpected situation occurs + retry_seconds = timeout + sleep_interval = 2 + while True: + if os.path.exists(path): + return True + else: + if retry_seconds <= 0: + LOG.warn(_('Discover file retries exhausted.')) + return False + else: + time.sleep(sleep_interval) + retry_seconds = retry_seconds - sleep_interval + + def _is_cloneable_share(self, image_location): + """Finds if the image at location is cloneable. + + WebNFS url format with relative-path is supported. + Accepting all characters in path-names and checking + against the mounted shares which will contain only + allowed path segments. + """ + + nfs_loc_pattern =\ + '^nfs://(([\w\-\.]+:{1}[\d]+|[\w\-\.]+)(/[^\/].*)*(/[^\/\\\\]+)$)' + matched = re.match(nfs_loc_pattern, image_location, flags=0) + if not matched: + LOG.debug(_('Image location not in the' + ' expected format %s'), image_location) + return None + conn = matched.group(2) + dir = matched.group(3) or '/' + return self._check_share_in_use(conn, dir) + + def _share_match_for_ip(self, ip, shares): + """Returns the share that is served by ip. + + Multiple shares can have same dir path but + can be served using different ips. It finds the + share which is served by ip on same nfs server. + """ + raise NotImplementedError() + + def _check_share_in_use(self, conn, dir): + """Checks if share is cinder mounted and returns it. """ + try: + if conn: + host = conn.split(':')[0] + ipv4 = socket.gethostbyname(host) + share_candidates = [] + for sh in self._mounted_shares: + sh_exp = sh.split(':')[1] + if sh_exp == dir: + share_candidates.append(sh) + if share_candidates: + LOG.debug(_('Found possible share matches %s'), + share_candidates) + return self._share_match_for_ip(ipv4, share_candidates) + except Exception: + LOG.warn(_("Unexpected exception while short listing used share.")) + return None + + def _construct_image_nfs_url(self, image_location): + """Construct direct url for nfs backend. + + It creates direct url from image_location + which is a tuple with direct_url and locations. + Returns url with nfs scheme if nfs store + else returns url. It needs to be verified + by backend before use. + """ + + direct_url, locations = image_location + + # Locations will be always a list of one until + # bp multiple-image-locations is introduced + if not locations: + return direct_url + location = locations[0] + url = location['url'] + if not location['metadata']: + return url + location_type = location['metadata'].get('type') + if not location_type or location_type.lower() != "nfs": + return url + share_location = location['metadata'].get('share_location') + mount_point = location['metadata'].get('mount_point') + if not share_location or not mount_point: + return url + url_parse = urlparse.urlparse(url) + abs_path = os.path.join(url_parse.netloc, url_parse.path) + rel_path = os.path.relpath(abs_path, mount_point) + direct_url = "%s/%s" % (share_location, rel_path) + return direct_url class NetAppDirectNfsDriver (NetAppNFSDriver): @@ -205,6 +592,7 @@ class NetAppDirectNfsDriver (NetAppNFSDriver): super(NetAppDirectNfsDriver, self).__init__(*args, **kwargs) def do_setup(self, context): + super(NetAppDirectNfsDriver, self).do_setup(context) self._context = context self.check_for_setup_error() self._client = self._get_client() @@ -214,14 +602,8 @@ class NetAppDirectNfsDriver (NetAppNFSDriver): """Returns an error if prerequisites aren't met.""" self._check_flags() - def _clone_volume(self, volume_name, clone_name, volume_id): - """Clones mounted volume on NetApp filer.""" - raise NotImplementedError() - def _check_flags(self): - """Raises error if any required configuration flag for NetApp - filer is missing. - """ + """Raises error if any required configuration flag is missing.""" required_flags = ['netapp_login', 'netapp_password', 'netapp_server_hostname', @@ -259,6 +641,28 @@ class NetAppDirectNfsDriver (NetAppNFSDriver): minor = res.get_child_content('minor-version') return (major, minor) + def _get_export_ip_path(self, volume_id=None, share=None): + """Returns export ip and path. + + One of volume id or share is used to return the values. + """ + + if volume_id: + host_ip = self._get_host_ip(volume_id) + export_path = self._get_export_path(volume_id) + elif share: + host_ip = share.split(':')[0] + export_path = share.split(':')[1] + else: + raise exception.InvalidInput('None of vol id or share specified.') + return (host_ip, export_path) + + def _create_file_usage_req(self, path): + """Creates the request element for file_usage_get.""" + file_use = NaElement.create_node_with_children( + 'file-usage-get', **{'path': path}) + return file_use + class NetAppDirectCmodeNfsDriver (NetAppDirectNfsDriver): """Executes commands related to volumes on c mode.""" @@ -355,14 +759,19 @@ class NetAppDirectCmodeNfsDriver (NetAppDirectNfsDriver): sorted(containers, key=lambda x: x[1], reverse=True)] return containers - def _clone_volume(self, volume_name, clone_name, volume_id): + def _clone_volume(self, volume_name, clone_name, + volume_id, share=None): """Clones mounted volume on NetApp Cluster.""" - host_ip = self._get_host_ip(volume_id) - export_path = self._get_export_path(volume_id) + (vserver, exp_volume) = self._get_vserver_and_exp_vol(volume_id, share) + self._clone_file(exp_volume, volume_name, clone_name, vserver) + + def _get_vserver_and_exp_vol(self, volume_id=None, share=None): + """Gets the vserver and export volume for share.""" + (host_ip, export_path) = self._get_export_ip_path(volume_id, share) ifs = self._get_if_info_by_ip(host_ip) vserver = ifs[0].get_child_content('vserver') exp_volume = self._get_vol_by_junc_vserver(vserver, export_path) - self._clone_file(exp_volume, volume_name, clone_name, vserver) + return (vserver, exp_volume) def _get_if_info_by_ip(self, ip): """Gets the network interface info by ip.""" @@ -380,6 +789,20 @@ class NetAppDirectCmodeNfsDriver (NetAppDirectNfsDriver): _('No interface found on cluster for ip %s') % (ip)) + def _get_verver_ips(self, vserver): + """Get ips for the vserver.""" + result = na_utils.invoke_api( + self._client, api_name='net-interface-get-iter', + is_iter=True, tunnel=vserver) + if_list = [] + for res in result: + records = res.get_child_content('num-records') + if records > 0: + attr_list = res['attributes-list'] + ifs = attr_list.get_children() + if_list.extend(ifs) + return if_list + def _get_vol_by_junc_vserver(self, vserver, junction): """Gets the volume by junction path and vserver.""" vol_iter = NaElement('volume-get-iter') @@ -410,7 +833,7 @@ class NetAppDirectCmodeNfsDriver (NetAppDirectNfsDriver): def _clone_file(self, volume, src_path, dest_path, vserver=None): """Clones file on vserver.""" - msg = _("""Cloning with params volume %(volume)s,src %(src_path)s, + msg = _("""Cloning with params volume %(volume)s, src %(src_path)s, dest %(dest_path)s, vserver %(vserver)s""") msg_fmt = {'volume': volume, 'src_path': src_path, 'dest_path': dest_path, 'vserver': vserver} @@ -486,10 +909,14 @@ class NetAppDirectCmodeNfsDriver (NetAppDirectNfsDriver): LOG.warn(_("No shares found hence skipping ssc refresh.")) return mnt_share_vols = set() + vs_ifs = self._get_verver_ips(self.vserver) for vol in vols['all']: for sh in self._mounted_shares: + host = sh.split(':')[0] junction = sh.split(':')[1] - if junction == vol.id['junction_path']: + ipv4 = socket.gethostbyname(host) + if (self._ip_in_ifs(ipv4, vs_ifs) and + junction == vol.id['junction_path']): mnt_share_vols.add(vol) vol.export['path'] = sh break @@ -497,6 +924,66 @@ class NetAppDirectCmodeNfsDriver (NetAppDirectNfsDriver): vols[key] = vols[key] & mnt_share_vols self.ssc_vols = vols + def _ip_in_ifs(self, ip, api_ifs): + """Checks if ip is listed for ifs in api format.""" + if api_ifs is None: + return False + for ifc in api_ifs: + ifc_ip = ifc.get_child_content("address") + if ifc_ip == ip: + return True + return False + + def _shortlist_del_eligible_files(self, share, old_files): + """Prepares list of eligible files to be deleted from cache.""" + file_list = [] + (vserver, exp_volume) = self._get_vserver_and_exp_vol( + volume_id=None, share=share) + for file in old_files: + path = '/vol/%s/%s' % (exp_volume, file) + u_bytes = self._get_cluster_file_usage(path, vserver) + file_list.append((file, u_bytes)) + LOG.debug(_('Shortlisted del elg files %s'), file_list) + return file_list + + def _get_cluster_file_usage(self, path, vserver): + """Gets the file unique bytes.""" + LOG.debug(_('Getting file usage for %s'), path) + file_use = NaElement.create_node_with_children( + 'file-usage-get', **{'path': path}) + res = self._invoke_successfully(file_use, vserver) + bytes = res.get_child_content('unique-bytes') + LOG.debug(_('file-usage for path %(path)s is %(bytes)s') + % {'path': path, 'bytes': bytes}) + return bytes + + def _share_match_for_ip(self, ip, shares): + """Returns the share that is served by ip. + + Multiple shares can have same dir path but + can be served using different ips. It finds the + share which is served by ip on same nfs server. + """ + ip_vserver = self._get_vserver_for_ip(ip) + if ip_vserver and shares: + for share in shares: + ip_sh = share.split(':')[0] + sh_vserver = self._get_vserver_for_ip(ip_sh) + if sh_vserver == ip_vserver: + LOG.debug(_('Share match found for ip %s'), ip) + return share + LOG.debug(_('No share match found for ip %s'), ip) + return None + + def _get_vserver_for_ip(self, ip): + """Get vserver for the mentioned ip.""" + try: + ifs = self._get_if_info_by_ip(ip) + vserver = ifs[0].get_child_content('vserver') + return vserver + except Exception: + return None + class NetAppDirect7modeNfsDriver (NetAppDirectNfsDriver): """Executes commands related to volumes on 7 mode.""" @@ -526,9 +1013,10 @@ class NetAppDirect7modeNfsDriver (NetAppDirectNfsDriver): result = server.invoke_successfully(na_element, True) return result - def _clone_volume(self, volume_name, clone_name, volume_id): + def _clone_volume(self, volume_name, clone_name, + volume_id, share=None): """Clones mounted volume with NetApp filer.""" - export_path = self._get_export_path(volume_id) + (host_ip, export_path) = self._get_export_ip_path(volume_id, share) storage_path = self._get_actual_path_for_export(export_path) target_path = '%s/%s' % (storage_path, clone_name) (clone_id, vol_uuid) = self._start_clone('%s/%s' % (storage_path, @@ -540,7 +1028,7 @@ class NetAppDirect7modeNfsDriver (NetAppDirectNfsDriver): except NaApiError as e: if e.code != 'UnknownCloneId': self._clear_clone(clone_id) - raise + raise e def _get_actual_path_for_export(self, export_path): """Gets the actual path on the filer for export path.""" @@ -632,3 +1120,63 @@ class NetAppDirect7modeNfsDriver (NetAppDirectNfsDriver): self._stats["driver_version"] = self.VERSION provide_ems(self, self._client, self._stats, netapp_backend, server_type="7mode") + + def _shortlist_del_eligible_files(self, share, old_files): + """Prepares list of eligible files to be deleted from cache.""" + file_list = [] + exp_volume = self._get_actual_path_for_export(share) + for file in old_files: + path = '/vol/%s/%s' % (exp_volume, file) + u_bytes = self._get_filer_file_usage(path) + file_list.append((file, u_bytes)) + LOG.debug(_('Shortlisted del elg files %s'), file_list) + return file_list + + def _get_filer_file_usage(self, path): + """Gets the file unique bytes.""" + LOG.debug(_('Getting file usage for %s'), path) + file_use = NaElement.create_node_with_children( + 'file-usage-get', **{'path': path}) + res = self._invoke_successfully(file_use) + bytes = res.get_child_content('unique-bytes') + LOG.debug(_('file-usage for path %(path)s is %(bytes)s') + % {'path': path, 'bytes': bytes}) + return bytes + + def _is_filer_ip(self, ip): + """Checks whether ip is on the same filer.""" + try: + ifconfig = NaElement('net-ifconfig-get') + res = self._invoke_successfully(ifconfig, None) + if_info = res.get_child_by_name('interface-config-info') + if if_info: + ifs = if_info.get_children() + for intf in ifs: + v4_addr = intf.get_child_by_name('v4-primary-address') + if v4_addr: + ip_info = v4_addr.get_child_by_name('ip-address-info') + if ip_info: + address = ip_info.get_child_content('address') + if ip == address: + return True + else: + continue + except Exception: + return False + return False + + def _share_match_for_ip(self, ip, shares): + """Returns the share that is served by ip. + + Multiple shares can have same dir path but + can be served using different ips. It finds the + share which is served by ip on same nfs server. + """ + if self._is_filer_ip(ip) and shares: + for share in shares: + ip_sh = share.split(':')[0] + if self._is_filer_ip(ip_sh): + LOG.debug(_('Share match found for ip %s'), ip) + return share + LOG.debug(_('No share match found for ip %s'), ip) + return None diff --git a/cinder/volume/drivers/netapp/options.py b/cinder/volume/drivers/netapp/options.py index 5ae47f8d3..cac2a7fe4 100644 --- a/cinder/volume/drivers/netapp/options.py +++ b/cinder/volume/drivers/netapp/options.py @@ -75,3 +75,15 @@ netapp_7mode_opts = [ cfg.StrOpt('netapp_vfiler', default=None, help='Vfiler to use for provisioning'), ] + +netapp_img_cache_opts = [ + cfg.IntOpt('thres_avl_size_perc_start', + default=20, + help='Threshold available percent to start cache cleaning.'), + cfg.IntOpt('thres_avl_size_perc_stop', + default=60, + help='Threshold available percent to stop cache cleaning.'), + cfg.IntOpt('expiry_thres_minutes', + default=720, + help='Threshold minutes after which ' + 'cache file can be cleaned.'), ] diff --git a/cinder/volume/drivers/rbd.py b/cinder/volume/drivers/rbd.py index 6749af18a..541cdc792 100644 --- a/cinder/volume/drivers/rbd.py +++ b/cinder/volume/drivers/rbd.py @@ -720,6 +720,7 @@ class RBDDriver(driver.VolumeDriver): return False def clone_image(self, volume, image_location, image_id): + image_location = image_location[0] if image_location else None if image_location is None or not self._is_cloneable(image_location): return ({}, False) prefix, pool, image, snapshot = self._parse_location(image_location) diff --git a/etc/cinder/cinder.conf.sample b/etc/cinder/cinder.conf.sample index 4c839da3d..e50d8e46f 100644 --- a/etc/cinder/cinder.conf.sample +++ b/etc/cinder/cinder.conf.sample @@ -1255,6 +1255,18 @@ # Port number for the storage controller (integer value) #netapp_server_port=80 +# Threshold available percent to start cache cleaning. +# (integer value) +#thres_avl_size_perc_start=20 + +# Threshold available percent to stop cache cleaning. (integer +# value) +#thres_avl_size_perc_stop=60 + +# Threshold minutes after which cache file can be cleaned. +# (integer value) +#expiry_thres_minutes=720 + # Volume size multiplier to ensure while creation (floating # point value) #netapp_size_multiplier=1.2 @@ -1719,4 +1731,4 @@ #volume_dd_blocksize=1M -# Total option count: 367 +# Total option count: 370 diff --git a/etc/cinder/rootwrap.d/volume.filters b/etc/cinder/rootwrap.d/volume.filters index b5f0fdb2c..99f75ed6c 100644 --- a/etc/cinder/rootwrap.d/volume.filters +++ b/etc/cinder/rootwrap.d/volume.filters @@ -56,6 +56,7 @@ truncate: CommandFilter, truncate, root chmod: CommandFilter, chmod, root rm: CommandFilter, rm, root lvs: CommandFilter, lvs, root +find: CommandFilter, find, root # cinder/volume/drivers/glusterfs.py mv: CommandFilter, mv, root -- 2.45.2