From f888e412b0d0fdb0426045a9c55e0be0390f842c Mon Sep 17 00:00:00 2001 From: Navneet Singh Date: Thu, 13 Feb 2014 02:32:08 +0530 Subject: [PATCH] NetApp implementation for copy offload in clustered nfs driver 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 | 206 ++++++++++++++++++- cinder/volume/drivers/netapp/nfs.py | 261 +++++++++++++++++++++--- cinder/volume/drivers/netapp/options.py | 8 + etc/cinder/cinder.conf.sample | 6 + 4 files changed, 446 insertions(+), 35 deletions(-) diff --git a/cinder/tests/test_netapp_nfs.py b/cinder/tests/test_netapp_nfs.py index 471ecdeaa..d6ceb635c 100644 --- a/cinder/tests/test_netapp_nfs.py +++ b/cinder/tests/test_netapp_nfs.py @@ -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): diff --git a/cinder/volume/drivers/netapp/nfs.py b/cinder/volume/drivers/netapp/nfs.py index dc434030e..f0601ae0a 100644 --- a/cinder/volume/drivers/netapp/nfs.py +++ b/cinder/volume/drivers/netapp/nfs.py @@ -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.""" diff --git a/cinder/volume/drivers/netapp/options.py b/cinder/volume/drivers/netapp/options.py index 790ddd589..8995d438b 100644 --- a/cinder/volume/drivers/netapp/options.py +++ b/cinder/volume/drivers/netapp/options.py @@ -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) diff --git a/etc/cinder/cinder.conf.sample b/etc/cinder/cinder.conf.sample index 2593e5d45..35c2f2a22 100644 --- a/etc/cinder/cinder.conf.sample +++ b/etc/cinder/cinder.conf.sample @@ -1368,6 +1368,12 @@ # 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= + # 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. -- 2.45.2