]> review.fuel-infra Code Review - openstack-build/cinder-build.git/commitdiff
NetApp NFS efficient clone_image impl
authorNavneet Singh <singn@netapp.com>
Tue, 16 Jul 2013 05:13:55 +0000 (10:43 +0530)
committerNavneet Singh <singn@netapp.com>
Sun, 28 Jul 2013 12:48:55 +0000 (18:18 +0530)
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
cinder/tests/test_netapp_nfs.py
cinder/tests/test_rbd.py
cinder/volume/drivers/netapp/nfs.py
cinder/volume/drivers/netapp/options.py
cinder/volume/drivers/rbd.py
etc/cinder/cinder.conf.sample
etc/cinder/rootwrap.d/volume.filters

index 5ff8c4da52a56c7bead86dd980adccd1bbeb49e9..596fffcef96db0600bb567c02a0587877cbf79ab 100644 (file)
@@ -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."""
index 31e7114b4e28adbce43dd39170f16db80267fdce..4bdf855d2fb6724a88a1f0a9673afd0657f4862f 100644 (file)
 #    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':
index 843dbc85597409c39bb829c3254af0183608a2be..d782fdc7396b185dfb38dfbbcb326ec3c43b3276 100644 (file)
@@ -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)
index 477a4c11e578e348370916eaec68a79978e15bdd..e5599a3fea36933c5c39008bad55704f17e60ec7 100644 (file)
@@ -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
index 5ae47f8d3bb5aaf42c4fa75e7e769963ed3ac254..cac2a7fe40eeedb744db187835b6fdd07752198e 100644 (file)
@@ -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.'), ]
index 6749af18aee850fae03e6dda96b3445099665092..541cdc792cf30215c35939fa0a98991e563b2d9d 100644 (file)
@@ -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)
index 4c839da3d66e651d070f4d3d8e1cfa8ead61cc9c..e50d8e46f79c099da2d6dea363a0b855f1c54a2b 100644 (file)
 # 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
 #volume_dd_blocksize=1M
 
 
-# Total option count: 367
+# Total option count: 370
index b5f0fdb2c2b960a9941727090dff7969705a80ec..99f75ed6c5d945cc8d7271aa5e5fb20f3129a69d 100644 (file)
@@ -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