]> review.fuel-infra Code Review - openstack-build/cinder-build.git/commitdiff
NetApp implementation for copy offload in clustered nfs driver
authorNavneet Singh <singn@netapp.com>
Wed, 12 Feb 2014 21:02:08 +0000 (02:32 +0530)
committerAlex Meade <mr.alex.meade@gmail.com>
Wed, 5 Mar 2014 19:28:47 +0000 (14:28 -0500)
The Copy offload binary is a special RPC implemented in Ontap that
allows NFS clients to ask the server to copy data between
volumes in the same cluster efficiently. The special
binary will be developed by NetApp and distributed to its customers.
It will address two copy cases after efficient image clone failed.
First when image cache file is present in a different share than
the one holding volume. Second when glance is backed by nfs share
which is on the same cluster as nfs driver backend. Instead
of regular http download the copy offload workflow will be used to copy
image to share and volume. Resubmitting it as there was a problem
with build in previous submission.

Change-Id: I0847d47cecdec8be2ade06d0ea944cc3fa6f476b
Implements: blueprint copyoffload

cinder/tests/test_netapp_nfs.py
cinder/volume/drivers/netapp/nfs.py
cinder/volume/drivers/netapp/options.py
etc/cinder/cinder.conf.sample

index 471ecdeaa7bf6171e36002ccd3aa9c930380b2e1..d6ceb635cd9333c34f42cbce1895b17be625df97 100644 (file)
@@ -16,6 +16,7 @@
 """Unit tests for the NetApp-specific NFS driver module."""
 
 from lxml import etree
+import mock
 import mox
 from mox import IgnoreArg
 from mox import IsA
@@ -29,6 +30,7 @@ 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 cinder.volume.drivers.netapp import utils
 
 
 from oslo.config import cfg
@@ -751,8 +753,8 @@ class NetappDirectCmodeNfsDriverTestCase(test.TestCase):
     def test_check_share_in_use_incorrect_host(self):
         drv = self._driver
         mox = self.mox
-        mox.StubOutWithMock(drv, '_resolve_hostname')
-        drv._resolve_hostname(IgnoreArg()).AndRaise(Exception())
+        mox.StubOutWithMock(utils, 'resolve_hostname')
+        utils.resolve_hostname(IgnoreArg()).AndRaise(Exception())
         mox.ReplayAll()
         share = drv._check_share_in_use('incorrect:8989', '/dir')
         mox.VerifyAll()
@@ -763,9 +765,9 @@ class NetappDirectCmodeNfsDriverTestCase(test.TestCase):
         drv = self._driver
         mox = self.mox
         drv._mounted_shares = ['127.0.0.1:/dir/share']
-        mox.StubOutWithMock(drv, '_resolve_hostname')
+        mox.StubOutWithMock(utils, 'resolve_hostname')
         mox.StubOutWithMock(drv, '_share_match_for_ip')
-        drv._resolve_hostname(IgnoreArg()).AndReturn('10.22.33.44')
+        utils.resolve_hostname(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()
@@ -794,6 +796,202 @@ class NetappDirectCmodeNfsDriverTestCase(test.TestCase):
             self.fail("Unexpected direct url.")
 
 
+class NetappDirectCmodeNfsDriverOnlyTestCase(test.TestCase):
+    """Test direct NetApp C Mode driver only and not inherit."""
+
+    def setUp(self):
+        super(NetappDirectCmodeNfsDriverOnlyTestCase, self).setUp()
+        self._custom_setup()
+
+    def _custom_setup(self):
+        kwargs = {}
+        kwargs['netapp_mode'] = 'proxy'
+        kwargs['configuration'] = create_configuration()
+        self._driver = netapp_nfs.NetAppDirectCmodeNfsDriver(**kwargs)
+        self._driver.ssc_enabled = True
+        self._driver.configuration.netapp_copyoffload_tool_path = 'cof_path'
+
+    def test_copy_img_to_vol_copyoffload_success(self):
+        drv = self._driver
+        context = object()
+        volume = {'id': 'vol_id', 'name': 'name'}
+        image_service = object()
+        image_id = 'image_id'
+        drv._client = mock.Mock()
+        drv._client.get_api_version = mock.Mock(return_value=(1, 20))
+        drv._try_copyoffload = mock.Mock()
+        drv._get_provider_location = mock.Mock(return_value='share')
+        drv._get_vol_for_share = mock.Mock(return_value='vol')
+        drv._update_stale_vols = mock.Mock()
+
+        drv.copy_image_to_volume(context, volume, image_service, image_id)
+        drv._try_copyoffload.assert_called_once_with(context, volume,
+                                                     image_service,
+                                                     image_id)
+        drv._update_stale_vols.assert_called_once_with('vol')
+
+    def test_copy_img_to_vol_copyoffload_failure(self):
+        drv = self._driver
+        context = object()
+        volume = {'id': 'vol_id', 'name': 'name'}
+        image_service = object()
+        image_id = 'image_id'
+        drv._client = mock.Mock()
+        drv._client.get_api_version = mock.Mock(return_value=(1, 20))
+        drv._try_copyoffload = mock.Mock(side_effect=Exception())
+        netapp_nfs.NetAppNFSDriver.copy_image_to_volume = mock.Mock()
+        drv._get_provider_location = mock.Mock(return_value='share')
+        drv._get_vol_for_share = mock.Mock(return_value='vol')
+        drv._update_stale_vols = mock.Mock()
+
+        drv.copy_image_to_volume(context, volume, image_service, image_id)
+        drv._try_copyoffload.assert_called_once_with(context, volume,
+                                                     image_service,
+                                                     image_id)
+        netapp_nfs.NetAppNFSDriver.copy_image_to_volume.\
+            assert_called_once_with(context, volume, image_service, image_id)
+        drv._update_stale_vols.assert_called_once_with('vol')
+
+    def test_copyoffload_frm_cache_success(self):
+        drv = self._driver
+        context = object()
+        volume = {'id': 'vol_id', 'name': 'name'}
+        image_service = object()
+        image_id = 'image_id'
+        drv._find_image_in_cache = mock.Mock(return_value=[('share', 'img')])
+        drv._copy_from_cache = mock.Mock(return_value=True)
+
+        drv._try_copyoffload(context, volume, image_service, image_id)
+        drv._copy_from_cache.assert_called_once_with(volume,
+                                                     image_id,
+                                                     [('share', 'img')])
+
+    def test_copyoffload_frm_img_service_success(self):
+        drv = self._driver
+        context = object()
+        volume = {'id': 'vol_id', 'name': 'name'}
+        image_service = object()
+        image_id = 'image_id'
+        drv._client = mock.Mock()
+        drv._client.get_api_version = mock.Mock(return_value=(1, 20))
+        drv._find_image_in_cache = mock.Mock(return_value=[])
+        drv._copy_from_img_service = mock.Mock()
+
+        drv._try_copyoffload(context, volume, image_service, image_id)
+        drv._copy_from_img_service.assert_called_once_with(context,
+                                                           volume,
+                                                           image_service,
+                                                           image_id)
+
+    def test_cache_copyoffload_workflow_success(self):
+        drv = self._driver
+        volume = {'id': 'vol_id', 'name': 'name', 'size': 1}
+        image_id = 'image_id'
+        cache_result = [('ip1:/openstack', 'img-cache-imgid')]
+        drv._get_ip_verify_on_cluster = mock.Mock(return_value='ip1')
+        drv._get_host_ip = mock.Mock(return_value='ip2')
+        drv._get_export_path = mock.Mock(return_value='/exp_path')
+        drv._execute = mock.Mock()
+        drv._register_image_in_cache = mock.Mock()
+        drv._get_provider_location = mock.Mock(return_value='/share')
+        drv._post_clone_image = mock.Mock()
+
+        copied = drv._copy_from_cache(volume, image_id, cache_result)
+        self.assertTrue(copied)
+        drv._get_ip_verify_on_cluster.assert_any_call('ip1')
+        drv._get_export_path.assert_called_with('vol_id')
+        drv._execute.assert_called_once_with('cof_path', 'ip1', 'ip1',
+                                             '/openstack/img-cache-imgid',
+                                             '/exp_path/name',
+                                             run_as_root=False,
+                                             check_exit_code=0)
+        drv._post_clone_image.assert_called_with(volume)
+        drv._get_provider_location.assert_called_with('vol_id')
+
+    @mock.patch.object(image_utils, 'qemu_img_info')
+    def test_img_service_raw_copyoffload_workflow_success(self,
+                                                          mock_qemu_img_info):
+        drv = self._driver
+        volume = {'id': 'vol_id', 'name': 'name', 'size': 1}
+        image_id = 'image_id'
+        context = object()
+        image_service = mock.Mock()
+        image_service.get_location.return_value = ('nfs://ip1/openstack/img',
+                                                   None)
+        image_service.show.return_value = {'size': 1,
+                                           'disk_format': 'raw'}
+
+        drv._check_get_nfs_path_segs = mock.Mock(return_value=
+                                                 ('ip1', '/openstack'))
+        drv._get_ip_verify_on_cluster = mock.Mock(return_value='ip1')
+        drv._get_host_ip = mock.Mock(return_value='ip2')
+        drv._get_export_path = mock.Mock(return_value='/exp_path')
+        drv._get_provider_location = mock.Mock(return_value='share')
+        drv._execute = mock.Mock()
+        drv._get_mount_point_for_share = mock.Mock(return_value='mnt_point')
+        drv._discover_file_till_timeout = mock.Mock(return_value=True)
+        img_inf = mock.Mock()
+        img_inf.file_format = 'raw'
+        mock_qemu_img_info.return_value = img_inf
+        drv._check_share_can_hold_size = mock.Mock()
+        drv._move_nfs_file = mock.Mock(return_value=True)
+        drv._delete_file = mock.Mock()
+        drv._clone_file_dst_exists = mock.Mock()
+        drv._post_clone_image = mock.Mock()
+
+        drv._copy_from_img_service(context, volume, image_service, image_id)
+        drv._get_ip_verify_on_cluster.assert_any_call('ip1')
+        drv._get_export_path.assert_called_with('vol_id')
+        drv._check_share_can_hold_size.assert_called_with('share', 1)
+
+        assert drv._execute.call_count == 1
+        drv._post_clone_image.assert_called_with(volume)
+
+    @mock.patch.object(image_utils, 'convert_image')
+    @mock.patch.object(image_utils, 'qemu_img_info')
+    @mock.patch('os.path.exists')
+    def test_img_service_qcow2_copyoffload_workflow_success(self, mock_exists,
+                                                            mock_qemu_img_info,
+                                                            mock_cvrt_image):
+        drv = self._driver
+        volume = {'id': 'vol_id', 'name': 'name', 'size': 1}
+        image_id = 'image_id'
+        context = object()
+        image_service = mock.Mock()
+        image_service.get_location.return_value = ('nfs://ip1/openstack/img',
+                                                   None)
+        image_service.show.return_value = {'size': 1,
+                                           'disk_format': 'qcow2'}
+        drv._check_get_nfs_path_segs = mock.Mock(return_value=
+                                                 ('ip1', '/openstack'))
+
+        drv._get_ip_verify_on_cluster = mock.Mock(return_value='ip1')
+        drv._get_host_ip = mock.Mock(return_value='ip2')
+        drv._get_export_path = mock.Mock(return_value='/exp_path')
+        drv._get_provider_location = mock.Mock(return_value='share')
+        drv._execute = mock.Mock()
+        drv._get_mount_point_for_share = mock.Mock(return_value='mnt_point')
+        img_inf = mock.Mock()
+        img_inf.file_format = 'raw'
+        mock_qemu_img_info.return_value = img_inf
+        drv._check_share_can_hold_size = mock.Mock()
+
+        drv._move_nfs_file = mock.Mock(return_value=True)
+        drv._delete_file = mock.Mock()
+        drv._clone_file_dst_exists = mock.Mock()
+        drv._post_clone_image = mock.Mock()
+
+        drv._copy_from_img_service(context, volume, image_service, image_id)
+        drv._get_ip_verify_on_cluster.assert_any_call('ip1')
+        drv._get_export_path.assert_called_with('vol_id')
+        drv._check_share_can_hold_size.assert_called_with('share', 1)
+        assert mock_cvrt_image.call_count == 1
+        assert drv._execute.call_count == 1
+        assert drv._delete_file.call_count == 2
+        drv._clone_file_dst_exists.call_count == 1
+        drv._post_clone_image.assert_called_with(volume)
+
+
 class NetappDirect7modeNfsDriverTestCase(NetappDirectCmodeNfsDriverTestCase):
     """Test direct NetApp C Mode driver."""
     def _custom_setup(self):
index dc434030e1be19f881c530ffb5a3991e4da9fbd1..f0601ae0a4707e7ea00901cd4f6f7dc8f3ce0531 100644 (file)
@@ -19,10 +19,10 @@ Volume driver for NetApp NFS storage.
 import copy
 import os
 import re
-import socket
 from threading import Timer
 import time
 import urlparse
+import uuid
 
 from cinder import exception
 from cinder.image import image_utils
@@ -38,6 +38,7 @@ 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_nfs_extra_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
@@ -221,7 +222,8 @@ class NetAppNFSDriver(nfs.NfsDriver):
         """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'])
+        LOG.info(_('Copied image to volume %s using regular download.'),
+                 volume['name'])
         self._register_image_in_cache(volume, image_id)
 
     def _register_image_in_cache(self, volume, image_id):
@@ -260,7 +262,7 @@ class NetAppNFSDriver(nfs.NfsDriver):
             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)
+                LOG.info(_('Cloning from cache to destination %s'), dst)
                 self._clone_volume(src, dst, volume_id=None, share=share)
         _do_clone()
 
@@ -392,7 +394,7 @@ class NetAppNFSDriver(nfs.NfsDriver):
                 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'
+            LOG.info(_('Image cloning unsuccessful for image'
                        ' %(image_id)s. Message: %(msg)s')
                      % {'image_id': image_id, 'msg': msg})
             vol_path = self.local_path(volume)
@@ -428,7 +430,7 @@ class NetAppNFSDriver(nfs.NfsDriver):
 
     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)
+        LOG.info(_('Checking image clone %s from glance share.'), image_id)
         cloned = False
         image_location = self._construct_image_nfs_url(image_location)
         share = self._is_cloneable_share(image_location)
@@ -514,24 +516,31 @@ class NetAppNFSDriver(nfs.NfsDriver):
                     retry_seconds = retry_seconds - sleep_interval
 
     def _is_cloneable_share(self, image_location):
-        """Finds if the image at location is cloneable.
+        """Finds if the image at location is cloneable."""
+        conn, dr = self._check_get_nfs_path_segs(image_location)
+        return self._check_share_in_use(conn, dr)
 
-             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.
-        """
+    def _check_get_nfs_path_segs(self, image_location):
+        """Checks if the nfs path format is matched.
 
-        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)
+            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. Returns connection and dir details.
+        """
+        conn, dr = None, None
+        if image_location:
+            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)
+            else:
+                conn = matched.group(2)
+                dr = matched.group(3) or '/'
+        return (conn, dr)
 
     def _share_match_for_ip(self, ip, shares):
         """Returns the share that is served by ip.
@@ -547,7 +556,7 @@ class NetAppNFSDriver(nfs.NfsDriver):
         try:
             if conn:
                 host = conn.split(':')[0]
-                ip = self._resolve_hostname(host)
+                ip = na_utils.resolve_hostname(host)
                 share_candidates = []
                 for sh in self._mounted_shares:
                     sh_exp = sh.split(':')[1]
@@ -572,6 +581,8 @@ class NetAppNFSDriver(nfs.NfsDriver):
         """
 
         direct_url, locations = image_location
+        if not direct_url and not locations:
+            raise exception.NotFound(_('Image location not present.'))
 
         # Locations will be always a list of one until
         # bp multiple-image-locations is introduced
@@ -604,11 +615,29 @@ class NetAppNFSDriver(nfs.NfsDriver):
         """Checks if share is compatible with volume to host it."""
         raise NotImplementedError()
 
-    def _resolve_hostname(self, hostname):
-        """Resolves hostname to IP address."""
-        res = socket.getaddrinfo(hostname, None)[0]
-        family, socktype, proto, canonname, sockaddr = res
-        return sockaddr[0]
+    def _check_share_can_hold_size(self, share, size):
+        """Checks if volume can hold image with size."""
+        tot_size, tot_available, tot_allocated = self._get_capacity_info(share)
+        if tot_available < size:
+            msg = _("Container size smaller than required file size.")
+            raise exception.VolumeDriverException(msg)
+
+    def _move_nfs_file(self, source_path, dest_path):
+        """Moves source to destination."""
+        @utils.synchronized(dest_path, external=True)
+        def _move_file(src, dst):
+            if os.path.exists(dst):
+                LOG.warn(_("Destination %s already exists."), dst)
+                return False
+            self._execute('mv', src, dst, run_as_root=True)
+            return True
+
+        try:
+            return _move_file(source_path, dest_path)
+        except Exception as e:
+            LOG.warn(_('Exception moving file %(src)s. Message - %(e)s')
+                     % {'src': source_path, 'e': e})
+        return False
 
 
 class NetAppDirectNfsDriver (NetAppNFSDriver):
@@ -695,6 +724,7 @@ class NetAppDirectCmodeNfsDriver (NetAppDirectNfsDriver):
     def __init__(self, *args, **kwargs):
         super(NetAppDirectCmodeNfsDriver, self).__init__(*args, **kwargs)
         self.configuration.append_config_values(netapp_cluster_opts)
+        self.configuration.append_config_values(netapp_nfs_extra_opts)
 
     def _do_custom_setup(self, client):
         """Do the customized set up on client for cluster mode."""
@@ -804,8 +834,8 @@ class NetAppDirectCmodeNfsDriver (NetAppDirectNfsDriver):
         net_if_iter.add_new_child('max-records', '10')
         query = NaElement('query')
         net_if_iter.add_child_elem(query)
-        query.add_node_with_children('net-interface-info',
-                                     **{'address': self._resolve_hostname(ip)})
+        query.add_node_with_children(
+            'net-interface-info', **{'address': na_utils.resolve_hostname(ip)})
         result = self._invoke_successfully(net_if_iter)
         if result.get_child_content('num-records') and\
                 int(result.get_child_content('num-records')) >= 1:
@@ -857,7 +887,8 @@ class NetAppDirectCmodeNfsDriver (NetAppDirectNfsDriver):
                                    %(vserver)s and junction path %(junction)s
                                    """) % msg_fmt)
 
-    def _clone_file(self, volume, src_path, dest_path, vserver=None):
+    def _clone_file(self, volume, src_path, dest_path, vserver=None,
+                    dest_exists=False):
         """Clones file on vserver."""
         msg = _("""Cloning with params volume %(volume)s, src %(src_path)s,
                     dest %(dest_path)s, vserver %(vserver)s""")
@@ -868,6 +899,9 @@ class NetAppDirectCmodeNfsDriver (NetAppDirectNfsDriver):
             'clone-create',
             **{'volume': volume, 'source-path': src_path,
                 'destination-path': dest_path})
+        major, minor = self._client.get_api_version()
+        if major == 1 and minor >= 20 and dest_exists:
+            clone_create.add_new_child('destination-exists', 'true')
         self._invoke_successfully(clone_create, vserver)
 
     def _update_volume_stats(self):
@@ -946,7 +980,7 @@ class NetAppDirectCmodeNfsDriver (NetAppDirectNfsDriver):
             for sh in self._mounted_shares:
                 host = sh.split(':')[0]
                 junction = sh.split(':')[1]
-                ip = self._resolve_hostname(host)
+                ip = na_utils.resolve_hostname(host)
                 if (self._ip_in_ifs(ip, vs_ifs) and
                         junction == vol.id['junction_path']):
                     mnt_share_vols.add(vol)
@@ -1059,6 +1093,171 @@ class NetAppDirectCmodeNfsDriver (NetAppDirectNfsDriver):
             if netapp_vol:
                 self._update_stale_vols(volume=netapp_vol)
 
+    def copy_image_to_volume(self, context, volume, image_service, image_id):
+        """Fetch the image from image_service and write it to the volume."""
+        copy_success = False
+        try:
+            major, minor = self._client.get_api_version()
+            col_path = self.configuration.netapp_copyoffload_tool_path
+            if (major == 1 and minor >= 20 and col_path):
+                self._try_copyoffload(context, volume, image_service, image_id)
+                copy_success = True
+                LOG.info(_('Copied image %(img)s to volume %(vol)s using copy'
+                           ' offload workflow.')
+                         % {'img': image_id, 'vol': volume['id']})
+            else:
+                LOG.debug(_("Copy offload either not configured or"
+                            " unsupported."))
+        except Exception as e:
+            LOG.exception(_('Copy offload workflow unsuccessful. %s'), e)
+        finally:
+            if not copy_success:
+                super(NetAppDirectCmodeNfsDriver, self).copy_image_to_volume(
+                    context, volume, image_service, image_id)
+            if self.ssc_enabled:
+                sh = self._get_provider_location(volume['id'])
+                self._update_stale_vols(self._get_vol_for_share(sh))
+
+    def _try_copyoffload(self, context, volume, image_service, image_id):
+        """Tries server side file copy offload."""
+        copied = False
+        cache_result = self._find_image_in_cache(image_id)
+        if cache_result:
+            copied = self._copy_from_cache(volume, image_id, cache_result)
+        if not cache_result or not copied:
+            self._copy_from_img_service(context, volume, image_service,
+                                        image_id)
+
+    def _get_ip_verify_on_cluster(self, host):
+        """Verifies if host on same cluster and returns ip."""
+        ip = na_utils.resolve_hostname(host)
+        vserver = self._get_vserver_for_ip(ip)
+        if not vserver:
+            raise exception.NotFound(_("No vserver owning the ip %s.") % ip)
+        return ip
+
+    def _copy_from_cache(self, volume, image_id, cache_result):
+        """Try copying image file_name from cached file_name."""
+        LOG.debug(_("Trying copy from cache using copy offload."))
+        copied = False
+        for res in cache_result:
+            try:
+                (share, file_name) = res
+                LOG.debug(_("Found cache file_name on share %s."), share)
+                if share != self._get_provider_location(volume['id']):
+                    col_path = self.configuration.netapp_copyoffload_tool_path
+                    src_ip = self._get_ip_verify_on_cluster(
+                        share.split(':')[0])
+                    src_path = os.path.join(share.split(':')[1], file_name)
+                    dst_ip = self._get_ip_verify_on_cluster(self._get_host_ip(
+                        volume['id']))
+                    dst_path = os.path.join(
+                        self._get_export_path(volume['id']), volume['name'])
+                    self._execute(col_path, src_ip, dst_ip,
+                                  src_path, dst_path, run_as_root=False,
+                                  check_exit_code=0)
+                    self._register_image_in_cache(volume, image_id)
+                    LOG.debug(_("Copied image from cache to volume %s using"
+                                " copy offload."), volume['id'])
+                else:
+                    self._clone_file_dst_exists(share, file_name,
+                                                volume['name'],
+                                                dest_exists=True)
+                    LOG.debug(_("Copied image from cache to volume %s using"
+                                " cloning."), volume['id'])
+                self._post_clone_image(volume)
+                copied = True
+                break
+            except Exception as e:
+                LOG.exception(_('Error in workflow copy from cache. %s.'), e)
+        return copied
+
+    def _clone_file_dst_exists(self, share, src_name, dst_name,
+                               dest_exists=False):
+        """Clone file even if dest exists."""
+        (vserver, exp_volume) = self._get_vserver_and_exp_vol(share=share)
+        self._clone_file(exp_volume, src_name, dst_name, vserver,
+                         dest_exists=dest_exists)
+
+    def _copy_from_img_service(self, context, volume, image_service,
+                               image_id):
+        """Copies from the image service using copy offload."""
+        LOG.debug(_("Trying copy from image service using copy offload."))
+        image_loc = image_service.get_location(context, image_id)
+        image_loc = self._construct_image_nfs_url(image_loc)
+        conn, dr = self._check_get_nfs_path_segs(image_loc)
+        if conn:
+            src_ip = self._get_ip_verify_on_cluster(conn.split(':')[0])
+        else:
+            raise exception.NotFound(_("Source host details not found."))
+        (__, ___, img_file) = image_loc.rpartition('/')
+        src_path = os.path.join(dr, img_file)
+        dst_ip = self._get_ip_verify_on_cluster(self._get_host_ip(
+            volume['id']))
+        # tmp file is required to deal with img formats
+        tmp_img_file = str(uuid.uuid4())
+        col_path = self.configuration.netapp_copyoffload_tool_path
+        img_info = image_service.show(context, image_id)
+        dst_share = self._get_provider_location(volume['id'])
+        self._check_share_can_hold_size(dst_share, img_info['size'])
+
+        try:
+            # If src and dst share not equal
+            if (('%s:%s' % (src_ip, dr)) !=
+                    ('%s:%s' % (dst_ip, self._get_export_path(volume['id'])))):
+                dst_img_serv_path = os.path.join(
+                    self._get_export_path(volume['id']), tmp_img_file)
+                self._execute(col_path, src_ip, dst_ip, src_path,
+                              dst_img_serv_path, run_as_root=False,
+                              check_exit_code=0)
+            else:
+                self._clone_file_dst_exists(dst_share, img_file, tmp_img_file)
+            dst_dir = self._get_mount_point_for_share(dst_share)
+            dst_img_local = os.path.join(dst_dir, tmp_img_file)
+            self._discover_file_till_timeout(dst_img_local, timeout=120)
+            LOG.debug(_('Copied image %(img)s to tmp file %(tmp)s.')
+                      % {'img': image_id, 'tmp': tmp_img_file})
+            dst_img_cache_local = os.path.join(dst_dir,
+                                               'img-cache-%s' % (image_id))
+            if img_info['disk_format'] == 'raw':
+                LOG.debug(_('Image is raw %s.'), image_id)
+                self._clone_file_dst_exists(dst_share, tmp_img_file,
+                                            volume['name'], dest_exists=True)
+                self._move_nfs_file(dst_img_local, dst_img_cache_local)
+                LOG.debug(_('Copied raw image %(img)s to volume %(vol)s.')
+                          % {'img': image_id, 'vol': volume['id']})
+            else:
+                LOG.debug(_('Image will be converted to raw %s.'), image_id)
+                img_conv = str(uuid.uuid4())
+                dst_img_conv_local = os.path.join(dst_dir, img_conv)
+
+                # Checking against image size which is approximate check
+                self._check_share_can_hold_size(dst_share, img_info['size'])
+                try:
+                    image_utils.convert_image(dst_img_local,
+                                              dst_img_conv_local, 'raw')
+                    data = image_utils.qemu_img_info(dst_img_conv_local)
+                    if data.file_format != "raw":
+                        raise exception.InvalidResults(
+                            _("Converted to raw, but format is now %s.")
+                            % data.file_format)
+                    else:
+                        self._clone_file_dst_exists(dst_share, img_conv,
+                                                    volume['name'],
+                                                    dest_exists=True)
+                        self._move_nfs_file(dst_img_conv_local,
+                                            dst_img_cache_local)
+                        LOG.debug(_('Copied locally converted raw image'
+                                    ' %(img)s to volume %(vol)s.')
+                                  % {'img': image_id, 'vol': volume['id']})
+                finally:
+                    if os.path.exists(dst_img_conv_local):
+                        self._delete_file(dst_img_conv_local)
+            self._post_clone_image(volume)
+        finally:
+            if os.path.exists(dst_img_local):
+                self._delete_file(dst_img_local)
+
 
 class NetAppDirect7modeNfsDriver (NetAppDirectNfsDriver):
     """Executes commands related to volumes on 7 mode."""
index 790ddd58981cd9b5571516d8cc7c30b4884d5c2f..8995d438b5350f10173843c0f6559c77cb17ee4f 100644 (file)
@@ -162,6 +162,13 @@ netapp_eseries_opts = [
                      'currently supported. Specify the value of this option to'
                      ' be a comma separated list of disk pool names to be used'
                      ' for provisioning.')), ]
+netapp_nfs_extra_opts = [
+    cfg.StrOpt('netapp_copyoffload_tool_path',
+               default=None,
+               help=('This option specifies the path of the NetApp copy '
+                     'offload tool binary. Ensure that the binary has execute '
+                     'permissions set which allow the effective user of the '
+                     'cinder-volume process to execute the file.')), ]
 
 CONF = cfg.CONF
 CONF.register_opts(netapp_proxy_opts)
@@ -173,3 +180,4 @@ CONF.register_opts(netapp_7mode_opts)
 CONF.register_opts(netapp_provisioning_opts)
 CONF.register_opts(netapp_img_cache_opts)
 CONF.register_opts(netapp_eseries_opts)
+CONF.register_opts(netapp_nfs_extra_opts)
index 2593e5d450449df734c72b59380a3701cbfc4347..35c2f2a224df13b721eebc3d3490db37b54157e4 100644 (file)
 # NFS share. (integer value)
 #expiry_thres_minutes=720
 
+# This option specifies the path of the NetApp copy offload
+# tool binary. Ensure that the binary has execute permissions
+# set which allow the effective user of the cinder-volume
+# process to execute the file. (string value)
+#netapp_copyoffload_tool_path=<None>
+
 # The quantity to be multiplied by the requested volume size
 # to ensure enough space is available on the virtual storage
 # server (Vserver) to fulfill the volume creation request.