]> review.fuel-infra Code Review - openstack-build/cinder-build.git/commitdiff
Add driver for using files on a generic NFS server as virtual block devices
authorBen Swartzlander <bswartz@netapp.com>
Mon, 13 Aug 2012 20:57:50 +0000 (16:57 -0400)
committerBen Swartzlander <bswartz@netapp.com>
Wed, 15 Aug 2012 01:46:39 +0000 (21:46 -0400)
Add NetApp-specific NFS virtual block driver

blueprint nfs-files-as-virtual-block-devices
blueprint netapp-nfs-cinder-driver

Change-Id: I10ef6f3e230fcea2748152313d341db46bffce55

cinder/tests/test_netapp_nfs.py [new file with mode: 0644]
cinder/tests/test_nfs.py [new file with mode: 0644]
cinder/volume/netapp_nfs.py [new file with mode: 0644]
cinder/volume/nfs.py [new file with mode: 0644]

diff --git a/cinder/tests/test_netapp_nfs.py b/cinder/tests/test_netapp_nfs.py
new file mode 100644 (file)
index 0000000..1ebd842
--- /dev/null
@@ -0,0 +1,260 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright (c) 2012 NetApp, Inc.
+# All Rights Reserved.
+#
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
+#    not use this file except in compliance with the License. You may obtain
+#    a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#    License for the specific language governing permissions and limitations
+#    under the License.
+"""Unit tests for the NetApp-specific NFS driver module (netapp_nfs)"""
+
+from cinder import context
+from cinder import test
+from cinder import exception
+
+from cinder.volume import netapp_nfs
+from cinder.volume import netapp
+from cinder.volume import nfs
+from mox import IsA
+from mox import IgnoreArg
+from mox import MockObject
+
+import mox
+import suds
+import types
+
+
+class FakeVolume(object):
+    def __init__(self, size=0):
+        self.size = size
+        self.id = hash(self)
+        self.name = None
+
+    def __getitem__(self, key):
+        return self.__dict__[key]
+
+
+class FakeSnapshot(object):
+    def __init__(self, volume_size=0):
+        self.volume_name = None
+        self.name = None
+        self.volume_id = None
+        self.volume_size = volume_size
+        self.user_id = None
+        self.status = None
+
+    def __getitem__(self, key):
+        return self.__dict__[key]
+
+
+class FakeResponce(object):
+    def __init__(self, status):
+        """
+        :param status: Either 'failed' or 'passed'
+        """
+        self.Status = status
+
+        if status == 'failed':
+            self.Reason = 'Sample error'
+
+
+class NetappNfsDriverTestCase(test.TestCase):
+    """Test case for NetApp specific NFS clone driver"""
+
+    def setUp(self):
+        self._driver = netapp_nfs.NetAppNFSDriver()
+        self._mox = mox.Mox()
+
+    def tearDown(self):
+        self._mox.UnsetStubs()
+
+    def test_check_for_setup_error(self):
+        mox = self._mox
+        drv = self._driver
+        required_flags = [
+                'netapp_wsdl_url',
+                'netapp_login',
+                'netapp_password',
+                'netapp_server_hostname',
+                'netapp_server_port'
+            ]
+
+        # check exception raises when flags are not set
+        self.assertRaises(exception.CinderException,
+                          drv.check_for_setup_error)
+
+        # set required flags
+        for flag in required_flags:
+            setattr(netapp.FLAGS, flag, 'val')
+
+        mox.StubOutWithMock(nfs.NfsDriver, 'check_for_setup_error')
+        nfs.NfsDriver.check_for_setup_error()
+        mox.ReplayAll()
+
+        drv.check_for_setup_error()
+
+        mox.VerifyAll()
+
+        # restore initial FLAGS
+        for flag in required_flags:
+            delattr(netapp.FLAGS, flag)
+
+    def test_do_setup(self):
+        mox = self._mox
+        drv = self._driver
+
+        mox.StubOutWithMock(drv, 'check_for_setup_error')
+        mox.StubOutWithMock(netapp_nfs.NetAppNFSDriver, '_get_client')
+
+        drv.check_for_setup_error()
+        netapp_nfs.NetAppNFSDriver._get_client()
+
+        mox.ReplayAll()
+
+        drv.do_setup(IsA(context.RequestContext))
+
+        mox.VerifyAll()
+
+    def test_create_snapshot(self):
+        """Test snapshot can be created and deleted"""
+        mox = self._mox
+        drv = self._driver
+
+        mox.StubOutWithMock(drv, '_clone_volume')
+        drv._clone_volume(IgnoreArg(), IgnoreArg(), IgnoreArg())
+        mox.ReplayAll()
+
+        drv.create_snapshot(FakeSnapshot())
+
+        mox.VerifyAll()
+
+    def test_create_volume_from_snapshot(self):
+        """Tests volume creation from snapshot"""
+        drv = self._driver
+        mox = self._mox
+        volume = FakeVolume(1)
+        snapshot = FakeSnapshot(2)
+
+        self.assertRaises(exception.CinderException,
+                          drv.create_volume_from_snapshot,
+                          volume,
+                          snapshot)
+
+        snapshot = FakeSnapshot(1)
+
+        location = '127.0.0.1:/nfs'
+        expected_result = {'provider_location': location}
+        mox.StubOutWithMock(drv, '_clone_volume')
+        mox.StubOutWithMock(drv, '_get_volume_location')
+        drv._clone_volume(IgnoreArg(), IgnoreArg(), IgnoreArg())
+        drv._get_volume_location(IgnoreArg()).AndReturn(location)
+
+        mox.ReplayAll()
+
+        loc = drv.create_volume_from_snapshot(volume, snapshot)
+
+        self.assertEquals(loc, expected_result)
+
+        mox.VerifyAll()
+
+    def _prepare_delete_snapshot_mock(self, snapshot_exists):
+        drv = self._driver
+        mox = self._mox
+
+        mox.StubOutWithMock(drv, '_get_provider_location')
+        mox.StubOutWithMock(drv, '_volume_not_present')
+
+        if snapshot_exists:
+            mox.StubOutWithMock(drv, '_execute')
+            mox.StubOutWithMock(drv, '_get_volume_path')
+
+        drv._get_provider_location(IgnoreArg())
+        drv._volume_not_present(IgnoreArg(), IgnoreArg())\
+                                        .AndReturn(not snapshot_exists)
+
+        if snapshot_exists:
+            drv._get_volume_path(IgnoreArg(), IgnoreArg())
+            drv._execute('rm', None, run_as_root=True)
+
+        mox.ReplayAll()
+
+        return mox
+
+    def test_delete_existing_snapshot(self):
+        drv = self._driver
+        mox = self._prepare_delete_snapshot_mock(True)
+
+        drv.delete_snapshot(FakeSnapshot())
+
+        mox.VerifyAll()
+
+    def test_delete_missing_snapshot(self):
+        drv = self._driver
+        mox = self._prepare_delete_snapshot_mock(False)
+
+        drv.delete_snapshot(FakeSnapshot())
+
+        mox.VerifyAll()
+
+    def _prepare_clone_mock(self, status):
+        drv = self._driver
+        mox = self._mox
+
+        volume = FakeVolume()
+        setattr(volume, 'provider_location', '127.0.0.1:/nfs')
+
+        drv._client = MockObject(suds.client.Client)
+        drv._client.factory = MockObject(suds.client.Factory)
+        drv._client.service = MockObject(suds.client.ServiceSelector)
+
+        # ApiProxy() method is generated by ServiceSelector at runtime from the
+        # XML, so mocking is impossible.
+        setattr(drv._client.service,
+                'ApiProxy',
+                types.MethodType(lambda *args, **kwargs: FakeResponce(status),
+                                 suds.client.ServiceSelector))
+        mox.StubOutWithMock(drv, '_get_host_id')
+        mox.StubOutWithMock(drv, '_get_full_export_path')
+
+        drv._get_host_id(IgnoreArg()).AndReturn('10')
+        drv._get_full_export_path(IgnoreArg(), IgnoreArg()).AndReturn('/nfs')
+
+        return mox
+
+    def test_successfull_clone_volume(self):
+        drv = self._driver
+        mox = self._prepare_clone_mock('passed')
+
+        mox.ReplayAll()
+
+        volume_name = 'volume_name'
+        clone_name = 'clone_name'
+        volume_id = volume_name + str(hash(volume_name))
+
+        drv._clone_volume(volume_name, clone_name, volume_id)
+
+        mox.VerifyAll()
+
+    def test_failed_clone_volume(self):
+        drv = self._driver
+        mox = self._prepare_clone_mock('failed')
+
+        mox.ReplayAll()
+
+        volume_name = 'volume_name'
+        clone_name = 'clone_name'
+        volume_id = volume_name + str(hash(volume_name))
+
+        self.assertRaises(exception.CinderException,
+                          drv._clone_volume,
+                          volume_name, clone_name, volume_id)
+
+        mox.VerifyAll()
diff --git a/cinder/tests/test_nfs.py b/cinder/tests/test_nfs.py
new file mode 100644 (file)
index 0000000..6bf6a0f
--- /dev/null
@@ -0,0 +1,628 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright (c) 2012 NetApp, Inc.
+# All Rights Reserved.
+#
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
+#    not use this file except in compliance with the License. You may obtain
+#    a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#    License for the specific language governing permissions and limitations
+#    under the License.
+"""Unit tests for the NFS driver module"""
+
+import os
+import errno
+import __builtin__
+
+import mox as mox_lib
+from mox import IsA
+from mox import IgnoreArg
+from mox import stubout
+
+from cinder import context
+from cinder import test
+from cinder.exception import ProcessExecutionError
+
+from cinder.volume import nfs
+
+
+class DumbVolume(object):
+    fields = {}
+
+    def __setitem__(self, key, value):
+        self.fields[key] = value
+
+    def __getitem__(self, item):
+        return self.fields[item]
+
+
+class NfsDriverTestCase(test.TestCase):
+    """Test case for NFS driver"""
+
+    TEST_NFS_EXPORT1 = 'nfs-host1:/export'
+    TEST_NFS_EXPORT2 = 'nfs-host2:/export'
+    TEST_SIZE_IN_GB = 1
+    TEST_MNT_POINT = '/mnt/nfs'
+    TEST_MNT_POINT_BASE = '/mnt/test'
+    TEST_LOCAL_PATH = '/mnt/nfs/volume-123'
+    TEST_FILE_NAME = 'test.txt'
+    TEST_SHARES_CONFIG_FILE = '/etc/cinder/test-shares.conf'
+    ONE_GB_IN_BYTES = 1024 * 1024 * 1024
+
+    def setUp(self):
+        self._driver = nfs.NfsDriver()
+        self._mox = mox_lib.Mox()
+        self.stubs = stubout.StubOutForTesting()
+
+    def tearDown(self):
+        self._mox.UnsetStubs()
+        self.stubs.UnsetAll()
+
+    def stub_out_not_replaying(self, obj, attr_name):
+        attr_to_replace = getattr(obj, attr_name)
+        stub = mox_lib.MockObject(attr_to_replace)
+        self.stubs.Set(obj, attr_name, stub)
+
+    def test_path_exists_should_return_true(self):
+        """_path_exists should return True if stat returns 0"""
+        mox = self._mox
+        drv = self._driver
+
+        mox.StubOutWithMock(drv, '_execute')
+        drv._execute('stat', self.TEST_FILE_NAME, run_as_root=True)
+
+        mox.ReplayAll()
+
+        self.assertTrue(drv._path_exists(self.TEST_FILE_NAME))
+
+        mox.VerifyAll()
+
+    def test_path_exists_should_return_false(self):
+        """_path_exists should return True if stat doesn't return 0"""
+        mox = self._mox
+        drv = self._driver
+
+        mox.StubOutWithMock(drv, '_execute')
+        drv._execute('stat', self.TEST_FILE_NAME, run_as_root=True).\
+            AndRaise(ProcessExecutionError(
+            stderr="stat: cannot stat `test.txt': No such file or directory"))
+
+        mox.ReplayAll()
+
+        self.assertFalse(drv._path_exists(self.TEST_FILE_NAME))
+
+        mox.VerifyAll()
+
+    def test_local_path(self):
+        """local_path common use case"""
+        nfs.FLAGS.nfs_mount_point_base = self.TEST_MNT_POINT_BASE
+        drv = self._driver
+
+        volume = DumbVolume()
+        volume['provider_location'] = self.TEST_NFS_EXPORT1
+        volume['name'] = 'volume-123'
+
+        self.assertEqual('/mnt/test/12118957640568004265/volume-123',
+                         drv.local_path(volume))
+
+    def test_mount_nfs_should_mount_correctly(self):
+        """_mount_nfs common case usage"""
+        mox = self._mox
+        drv = self._driver
+
+        mox.StubOutWithMock(drv, '_path_exists')
+        drv._path_exists(self.TEST_MNT_POINT).AndReturn(True)
+
+        mox.StubOutWithMock(drv, '_execute')
+        drv._execute('mount', '-t', 'nfs', self.TEST_NFS_EXPORT1,
+                     self.TEST_MNT_POINT, run_as_root=True)
+
+        mox.ReplayAll()
+
+        drv._mount_nfs(self.TEST_NFS_EXPORT1, self.TEST_MNT_POINT)
+
+        mox.VerifyAll()
+
+    def test_mount_nfs_should_suppress_already_mounted_error(self):
+        """_mount_nfs should suppress already mounted error if ensure=True
+        """
+        mox = self._mox
+        drv = self._driver
+
+        mox.StubOutWithMock(drv, '_path_exists')
+        drv._path_exists(self.TEST_MNT_POINT).AndReturn(True)
+
+        mox.StubOutWithMock(drv, '_execute')
+        drv._execute('mount', '-t', 'nfs', self.TEST_NFS_EXPORT1,
+                     self.TEST_MNT_POINT, run_as_root=True).\
+            AndRaise(ProcessExecutionError(
+                        stderr='is busy or already mounted'))
+
+        mox.ReplayAll()
+
+        drv._mount_nfs(self.TEST_NFS_EXPORT1, self.TEST_MNT_POINT, ensure=True)
+
+        mox.VerifyAll()
+
+    def test_mount_nfs_should_reraise_already_mounted_error(self):
+        """_mount_nfs should not suppress already mounted error if ensure=False
+        """
+        mox = self._mox
+        drv = self._driver
+
+        mox.StubOutWithMock(drv, '_path_exists')
+        drv._path_exists(self.TEST_MNT_POINT).AndReturn(True)
+
+        mox.StubOutWithMock(drv, '_execute')
+        drv._execute('mount', '-t', 'nfs', self.TEST_NFS_EXPORT1,
+                     self.TEST_MNT_POINT, run_as_root=True).\
+        AndRaise(ProcessExecutionError(stderr='is busy or already mounted'))
+
+        mox.ReplayAll()
+
+        self.assertRaises(ProcessExecutionError, drv._mount_nfs,
+                          self.TEST_NFS_EXPORT1, self.TEST_MNT_POINT,
+                          ensure=False)
+
+        mox.VerifyAll()
+
+    def test_mount_nfs_should_create_mountpoint_if_not_yet(self):
+        """_mount_nfs should create mountpoint if it doesn't exist"""
+        mox = self._mox
+        drv = self._driver
+
+        mox.StubOutWithMock(drv, '_path_exists')
+        drv._path_exists(self.TEST_MNT_POINT).AndReturn(False)
+
+        mox.StubOutWithMock(drv, '_execute')
+        drv._execute('mkdir', '-p', self.TEST_MNT_POINT)
+        drv._execute(*([IgnoreArg()] * 5), run_as_root=IgnoreArg())
+
+        mox.ReplayAll()
+
+        drv._mount_nfs(self.TEST_NFS_EXPORT1, self.TEST_MNT_POINT)
+
+        mox.VerifyAll()
+
+    def test_mount_nfs_should_not_create_mountpoint_if_already(self):
+        """_mount_nfs should not create mountpoint if it already exists"""
+        mox = self._mox
+        drv = self._driver
+
+        mox.StubOutWithMock(drv, '_path_exists')
+        drv._path_exists(self.TEST_MNT_POINT).AndReturn(True)
+
+        mox.StubOutWithMock(drv, '_execute')
+        drv._execute(*([IgnoreArg()] * 5), run_as_root=IgnoreArg())
+
+        mox.ReplayAll()
+
+        drv._mount_nfs(self.TEST_NFS_EXPORT1, self.TEST_MNT_POINT)
+
+        mox.VerifyAll()
+
+    def test_get_hash_str(self):
+        """_get_hash_str should calculation correct value"""
+        drv = self._driver
+
+        self.assertEqual('12118957640568004265',
+                         drv._get_hash_str(self.TEST_NFS_EXPORT1))
+
+    def test_get_mount_point_for_share(self):
+        """_get_mount_point_for_share should calculate correct value"""
+        drv = self._driver
+
+        nfs.FLAGS.nfs_mount_point_base = self.TEST_MNT_POINT_BASE
+
+        self.assertEqual('/mnt/test/12118957640568004265',
+                         drv._get_mount_point_for_share(self.TEST_NFS_EXPORT1))
+
+    def test_get_available_capacity_with_df(self):
+        """_get_available_capacity should calculate correct value"""
+        mox = self._mox
+        drv = self._driver
+
+        df_avail = 1490560
+        df_head = 'Filesystem 1K-blocks Used Available Use% Mounted on\n'
+        df_data = 'nfs-host:/export 2620544 996864 %d 41%% /mnt' % df_avail
+        df_output = df_head + df_data
+
+        setattr(nfs.FLAGS, 'nfs_disk_util', 'df')
+
+        mox.StubOutWithMock(drv, '_get_mount_point_for_share')
+        drv._get_mount_point_for_share(self.TEST_NFS_EXPORT1).\
+            AndReturn(self.TEST_MNT_POINT)
+
+        mox.StubOutWithMock(drv, '_execute')
+        drv._execute('df', '-P', '-B', '1', self.TEST_MNT_POINT,
+                     run_as_root=True).AndReturn((df_output, None))
+
+        mox.ReplayAll()
+
+        self.assertEquals(df_avail,
+                          drv._get_available_capacity(self.TEST_NFS_EXPORT1))
+
+        mox.VerifyAll()
+
+        delattr(nfs.FLAGS, 'nfs_disk_util')
+
+    def test_get_available_capacity_with_du(self):
+        """_get_available_capacity should calculate correct value"""
+        mox = self._mox
+        drv = self._driver
+
+        setattr(nfs.FLAGS, 'nfs_disk_util', 'du')
+
+        df_total_size = 2620544
+        df_used_size = 996864
+        df_avail_size = 1490560
+        df_title = 'Filesystem 1-blocks Used Available Use% Mounted on\n'
+        df_mnt_data = 'nfs-host:/export %d %d %d 41%% /mnt' % (df_total_size,
+                                                               df_used_size,
+                                                               df_avail_size)
+        df_output = df_title + df_mnt_data
+
+        du_used = 490560
+        du_output = '%d /mnt' % du_used
+
+        mox.StubOutWithMock(drv, '_get_mount_point_for_share')
+        drv._get_mount_point_for_share(self.TEST_NFS_EXPORT1).\
+            AndReturn(self.TEST_MNT_POINT)
+
+        mox.StubOutWithMock(drv, '_execute')
+        drv._execute('df', '-P', '-B', '1', self.TEST_MNT_POINT,
+                     run_as_root=True).\
+            AndReturn((df_output, None))
+        drv._execute('du', '-sb', '--apparent-size',
+                     '--exclude', '*snapshot*',
+                     self.TEST_MNT_POINT,
+                     run_as_root=True).AndReturn((du_output, None))
+
+        mox.ReplayAll()
+
+        self.assertEquals(df_total_size - du_used,
+                          drv._get_available_capacity(self.TEST_NFS_EXPORT1))
+
+        mox.VerifyAll()
+
+        delattr(nfs.FLAGS, 'nfs_disk_util')
+
+    def test_load_shares_config(self):
+        mox = self._mox
+        drv = self._driver
+
+        nfs.FLAGS.nfs_shares_config = self.TEST_SHARES_CONFIG_FILE
+
+        mox.StubOutWithMock(__builtin__, 'open')
+        config_data = []
+        config_data.append(self.TEST_NFS_EXPORT1)
+        config_data.append('#' + self.TEST_NFS_EXPORT2)
+        config_data.append('')
+        __builtin__.open(self.TEST_SHARES_CONFIG_FILE).AndReturn(config_data)
+        mox.ReplayAll()
+
+        shares = drv._load_shares_config()
+
+        self.assertEqual([self.TEST_NFS_EXPORT1], shares)
+
+        mox.VerifyAll()
+
+    def test_ensure_share_mounted(self):
+        """_ensure_share_mounted simple use case"""
+        mox = self._mox
+        drv = self._driver
+
+        mox.StubOutWithMock(drv, '_get_mount_point_for_share')
+        drv._get_mount_point_for_share(self.TEST_NFS_EXPORT1).\
+            AndReturn(self.TEST_MNT_POINT)
+
+        mox.StubOutWithMock(drv, '_mount_nfs')
+        drv._mount_nfs(self.TEST_NFS_EXPORT1, self.TEST_MNT_POINT, ensure=True)
+
+        mox.ReplayAll()
+
+        drv._ensure_share_mounted(self.TEST_NFS_EXPORT1)
+
+        mox.VerifyAll()
+
+    def test_ensure_shares_mounted_should_save_mounting_successfully(self):
+        """_ensure_shares_mounted should save share if mounted with success"""
+        mox = self._mox
+        drv = self._driver
+
+        mox.StubOutWithMock(drv, '_load_shares_config')
+        drv._load_shares_config().AndReturn([self.TEST_NFS_EXPORT1])
+        mox.StubOutWithMock(drv, '_ensure_share_mounted')
+        drv._ensure_share_mounted(self.TEST_NFS_EXPORT1)
+
+        mox.ReplayAll()
+
+        drv._ensure_shares_mounted()
+
+        self.assertEqual(1, len(drv._mounted_shares))
+        self.assertEqual(self.TEST_NFS_EXPORT1, drv._mounted_shares[0])
+
+        mox.VerifyAll()
+
+    def test_ensure_shares_mounted_should_not_save_mounting_with_error(self):
+        """_ensure_shares_mounted should not save share if failed to mount"""
+        mox = self._mox
+        drv = self._driver
+
+        mox.StubOutWithMock(drv, '_load_shares_config')
+        drv._load_shares_config().AndReturn([self.TEST_NFS_EXPORT1])
+        mox.StubOutWithMock(drv, '_ensure_share_mounted')
+        drv._ensure_share_mounted(self.TEST_NFS_EXPORT1).AndRaise(Exception())
+
+        mox.ReplayAll()
+
+        drv._ensure_shares_mounted()
+
+        self.assertEqual(0, len(drv._mounted_shares))
+
+        mox.VerifyAll()
+
+    def test_setup_should_throw_error_if_shares_config_not_configured(self):
+        """do_setup should throw error if shares config is not configured """
+        drv = self._driver
+
+        nfs.FLAGS.nfs_shares_config = self.TEST_SHARES_CONFIG_FILE
+
+        self.assertRaises(nfs.NfsException,
+                          drv.do_setup, IsA(context.RequestContext))
+
+    def test_setup_should_throw_exception_if_nfs_client_is_not_installed(self):
+        """do_setup should throw error if nfs client is not installed """
+        mox = self._mox
+        drv = self._driver
+
+        nfs.FLAGS.nfs_shares_config = self.TEST_SHARES_CONFIG_FILE
+
+        mox.StubOutWithMock(os.path, 'exists')
+        os.path.exists(self.TEST_SHARES_CONFIG_FILE).AndReturn(True)
+        mox.StubOutWithMock(drv, '_execute')
+        drv._execute('mount.nfs', check_exit_code=False).\
+            AndRaise(OSError(errno.ENOENT, 'No such file or directory'))
+
+        mox.ReplayAll()
+
+        self.assertRaises(nfs.NfsException,
+                          drv.do_setup, IsA(context.RequestContext))
+
+        mox.VerifyAll()
+
+    def test_find_share_should_throw_error_if_there_is_no_mounted_shares(self):
+        """_find_share should throw error if there is no mounted shares"""
+        drv = self._driver
+
+        drv._mounted_shares = []
+
+        self.assertRaises(nfs.NfsException, drv._find_share,
+                          self.TEST_SIZE_IN_GB)
+
+    def test_find_share(self):
+        """_find_share simple use case"""
+        mox = self._mox
+        drv = self._driver
+
+        drv._mounted_shares = [self.TEST_NFS_EXPORT1, self.TEST_NFS_EXPORT2]
+
+        mox.StubOutWithMock(drv, '_get_available_capacity')
+        drv._get_available_capacity(self.TEST_NFS_EXPORT1).\
+            AndReturn(2 * self.ONE_GB_IN_BYTES)
+        drv._get_available_capacity(self.TEST_NFS_EXPORT2).\
+            AndReturn(3 * self.ONE_GB_IN_BYTES)
+
+        mox.ReplayAll()
+
+        self.assertEqual(self.TEST_NFS_EXPORT2,
+                         drv._find_share(self.TEST_SIZE_IN_GB))
+
+        mox.VerifyAll()
+
+    def test_find_share_should_throw_error_if_there_is_no_enough_place(self):
+        """_find_share should throw error if there is no share to host vol"""
+        mox = self._mox
+        drv = self._driver
+
+        drv._mounted_shares = [self.TEST_NFS_EXPORT1, self.TEST_NFS_EXPORT2]
+
+        mox.StubOutWithMock(drv, '_get_available_capacity')
+        drv._get_available_capacity(self.TEST_NFS_EXPORT1).\
+            AndReturn(0)
+        drv._get_available_capacity(self.TEST_NFS_EXPORT2).\
+            AndReturn(0)
+
+        mox.ReplayAll()
+
+        self.assertRaises(nfs.NfsNoSuitableShareFound, drv._find_share,
+                          self.TEST_SIZE_IN_GB)
+
+        mox.VerifyAll()
+
+    def _simple_volume(self):
+        volume = DumbVolume()
+        volume['provider_location'] = '127.0.0.1:/mnt'
+        volume['name'] = 'volume_name'
+        volume['size'] = 10
+
+        return volume
+
+    def test_create_sparsed_volume(self):
+        mox = self._mox
+        drv = self._driver
+        volume = self._simple_volume()
+
+        setattr(nfs.FLAGS, 'nfs_sparsed_volumes', True)
+
+        mox.StubOutWithMock(drv, '_create_sparsed_file')
+        mox.StubOutWithMock(drv, '_set_rw_permissions_for_all')
+
+        drv._create_sparsed_file(IgnoreArg(), IgnoreArg())
+        drv._set_rw_permissions_for_all(IgnoreArg())
+
+        mox.ReplayAll()
+
+        drv._do_create_volume(volume)
+
+        mox.VerifyAll()
+
+        delattr(nfs.FLAGS, 'nfs_sparsed_volumes')
+
+    def test_create_nonsparsed_volume(self):
+        mox = self._mox
+        drv = self._driver
+        volume = self._simple_volume()
+
+        setattr(nfs.FLAGS, 'nfs_sparsed_volumes', False)
+
+        mox.StubOutWithMock(drv, '_create_regular_file')
+        mox.StubOutWithMock(drv, '_set_rw_permissions_for_all')
+
+        drv._create_regular_file(IgnoreArg(), IgnoreArg())
+        drv._set_rw_permissions_for_all(IgnoreArg())
+
+        mox.ReplayAll()
+
+        drv._do_create_volume(volume)
+
+        mox.VerifyAll()
+
+        delattr(nfs.FLAGS, 'nfs_sparsed_volumes')
+
+    def test_create_volume_should_ensure_nfs_mounted(self):
+        """create_volume should ensure shares provided in config are mounted"""
+        mox = self._mox
+        drv = self._driver
+
+        self.stub_out_not_replaying(nfs, 'LOG')
+        self.stub_out_not_replaying(drv, '_find_share')
+        self.stub_out_not_replaying(drv, '_do_create_volume')
+
+        mox.StubOutWithMock(drv, '_ensure_shares_mounted')
+        drv._ensure_shares_mounted()
+
+        mox.ReplayAll()
+
+        volume = DumbVolume()
+        volume['size'] = self.TEST_SIZE_IN_GB
+        drv.create_volume(volume)
+
+        mox.VerifyAll()
+
+    def test_create_volume_should_return_provider_location(self):
+        """create_volume should return provider_location with found share """
+        mox = self._mox
+        drv = self._driver
+
+        self.stub_out_not_replaying(nfs, 'LOG')
+        self.stub_out_not_replaying(drv, '_ensure_shares_mounted')
+        self.stub_out_not_replaying(drv, '_do_create_volume')
+
+        mox.StubOutWithMock(drv, '_find_share')
+        drv._find_share(self.TEST_SIZE_IN_GB).AndReturn(self.TEST_NFS_EXPORT1)
+
+        mox.ReplayAll()
+
+        volume = DumbVolume()
+        volume['size'] = self.TEST_SIZE_IN_GB
+        result = drv.create_volume(volume)
+        self.assertEqual(self.TEST_NFS_EXPORT1, result['provider_location'])
+
+        mox.VerifyAll()
+
+    def test_delete_volume(self):
+        """delete_volume simple test case"""
+        mox = self._mox
+        drv = self._driver
+
+        self.stub_out_not_replaying(drv, '_ensure_share_mounted')
+
+        volume = DumbVolume()
+        volume['name'] = 'volume-123'
+        volume['provider_location'] = self.TEST_NFS_EXPORT1
+
+        mox.StubOutWithMock(drv, 'local_path')
+        drv.local_path(volume).AndReturn(self.TEST_LOCAL_PATH)
+
+        mox.StubOutWithMock(drv, '_path_exists')
+        drv._path_exists(self.TEST_LOCAL_PATH).AndReturn(True)
+
+        mox.StubOutWithMock(drv, '_execute')
+        drv._execute('rm', '-f', self.TEST_LOCAL_PATH, run_as_root=True)
+
+        mox.ReplayAll()
+
+        drv.delete_volume(volume)
+
+        mox.VerifyAll()
+
+    def test_delete_should_ensure_share_mounted(self):
+        """delete_volume should ensure that corresponding share is mounted"""
+        mox = self._mox
+        drv = self._driver
+
+        self.stub_out_not_replaying(drv, '_execute')
+
+        volume = DumbVolume()
+        volume['name'] = 'volume-123'
+        volume['provider_location'] = self.TEST_NFS_EXPORT1
+
+        mox.StubOutWithMock(drv, '_ensure_share_mounted')
+        drv._ensure_share_mounted(self.TEST_NFS_EXPORT1)
+
+        mox.ReplayAll()
+
+        drv.delete_volume(volume)
+
+        mox.VerifyAll()
+
+    def test_delete_should_not_delete_if_provider_location_not_provided(self):
+        """delete_volume shouldn't try to delete if provider_location missed"""
+        mox = self._mox
+        drv = self._driver
+
+        self.stub_out_not_replaying(drv, '_ensure_share_mounted')
+
+        volume = DumbVolume()
+        volume['name'] = 'volume-123'
+        volume['provider_location'] = None
+
+        mox.StubOutWithMock(drv, '_execute')
+
+        mox.ReplayAll()
+
+        drv.delete_volume(volume)
+
+        mox.VerifyAll()
+
+    def test_delete_should_not_delete_if_there_is_no_file(self):
+        """delete_volume should not try to delete if file missed"""
+        mox = self._mox
+        drv = self._driver
+
+        self.stub_out_not_replaying(drv, '_ensure_share_mounted')
+
+        volume = DumbVolume()
+        volume['name'] = 'volume-123'
+        volume['provider_location'] = self.TEST_NFS_EXPORT1
+
+        mox.StubOutWithMock(drv, 'local_path')
+        drv.local_path(volume).AndReturn(self.TEST_LOCAL_PATH)
+
+        mox.StubOutWithMock(drv, '_path_exists')
+        drv._path_exists(self.TEST_LOCAL_PATH).AndReturn(False)
+
+        mox.StubOutWithMock(drv, '_execute')
+
+        mox.ReplayAll()
+
+        drv.delete_volume(volume)
+
+        mox.VerifyAll()
diff --git a/cinder/volume/netapp_nfs.py b/cinder/volume/netapp_nfs.py
new file mode 100644 (file)
index 0000000..dd69c6d
--- /dev/null
@@ -0,0 +1,266 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright (c) 2012 NetApp, Inc.
+# All Rights Reserved.
+#
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
+#    not use this file except in compliance with the License. You may obtain
+#    a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#    License for the specific language governing permissions and limitations
+#    under the License.
+"""
+Volume driver for NetApp NFS storage.
+"""
+
+import os
+import time
+import suds
+from suds.sax import text
+
+from cinder import exception
+from cinder import flags
+from cinder.openstack.common import cfg
+from cinder.openstack.common import log as logging
+from cinder.volume import nfs
+from cinder.volume.netapp import netapp_opts
+
+LOG = logging.getLogger("cinder.volume.driver")
+
+netapp_nfs_opts = [
+    cfg.IntOpt('synchronous_snapshot_create',
+               default=0,
+               help='Does snapshot creation call returns immediately')
+    ]
+
+FLAGS = flags.FLAGS
+FLAGS.register_opts(netapp_opts)
+FLAGS.register_opts(netapp_nfs_opts)
+
+
+class NetAppNFSDriver(nfs.NfsDriver):
+    """Executes commands relating to Volumes."""
+    def __init__(self, *args, **kwargs):
+        # NOTE(vish): db is set by Manager
+        self._execute = None
+        self._context = None
+        super(NetAppNFSDriver, self).__init__(*args, **kwargs)
+
+    def set_execute(self, execute):
+        self._execute = execute
+
+    def do_setup(self, context):
+        self._context = context
+        self.check_for_setup_error()
+        self._client = NetAppNFSDriver._get_client()
+
+    def check_for_setup_error(self):
+        """Returns an error if prerequisites aren't met"""
+        NetAppNFSDriver._check_dfm_flags()
+        super(NetAppNFSDriver, self).check_for_setup_error()
+
+    def create_volume_from_snapshot(self, volume, snapshot):
+        """Creates a volume from a snapshot."""
+        vol_size = volume.size
+        snap_size = snapshot.volume_size
+
+        if vol_size != snap_size:
+            msg = _('Cannot create volume of size %(vol_size)s from '
+                'snapshot of size %(snap_size)s')
+            raise exception.CinderException(msg % locals())
+
+        self._clone_volume(snapshot.name, volume.name, snapshot.volume_id)
+        share = self._get_volume_location(snapshot.volume_id)
+
+        return {'provider_location': share}
+
+    def create_snapshot(self, snapshot):
+        """Creates a snapshot."""
+        self._clone_volume(snapshot['volume_name'],
+                           snapshot['name'],
+                           snapshot['volume_id'])
+
+    def delete_snapshot(self, snapshot):
+        """Deletes a snapshot."""
+        nfs_mount = self._get_provider_location(snapshot.volume_id)
+
+        if self._volume_not_present(nfs_mount, snapshot.name):
+            return True
+
+        self._execute('rm', self._get_volume_path(nfs_mount, snapshot.name),
+                      run_as_root=True)
+
+    @staticmethod
+    def _check_dfm_flags():
+        """Raises error if any required configuration flag for OnCommand proxy
+        is missing."""
+        required_flags = ['netapp_wsdl_url',
+                          'netapp_login',
+                          'netapp_password',
+                          'netapp_server_hostname',
+                          'netapp_server_port']
+        for flag in required_flags:
+            if not getattr(FLAGS, flag, None):
+                raise exception.CinderException(_('%s is not set') % flag)
+
+    @staticmethod
+    def _get_client():
+        """Creates SOAP _client for ONTAP-7 DataFabric Service."""
+        client = suds.client.Client(FLAGS.netapp_wsdl_url,
+                                    username=FLAGS.netapp_login,
+                                    password=FLAGS.netapp_password)
+        soap_url = 'http://%s:%s/apis/soap/v1' % (
+                                          FLAGS.netapp_server_hostname,
+                                          FLAGS.netapp_server_port)
+        client.set_options(location=soap_url)
+
+        return client
+
+    def _get_volume_location(self, volume_id):
+        """Returns NFS mount address as <nfs_ip_address>:<nfs_mount_dir>"""
+        nfs_server_ip = self._get_host_ip(volume_id)
+        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"""
+        host_id = self._get_host_id(volume_id)
+        export_path = self._get_full_export_path(volume_id, host_id)
+
+        request = self._client.factory.create('Request')
+        request.Name = 'clone-start'
+
+        clone_start_args = ('<source-path>%s/%s</source-path>'
+                            '<destination-path>%s/%s</destination-path>')
+
+        request.Args = text.Raw(clone_start_args % (export_path,
+                                                    volume_name,
+                                                    export_path,
+                                                    clone_name))
+
+        resp = self._client.service.ApiProxy(Target=host_id,
+                                            Request=request)
+
+        if resp.Status == 'passed' and FLAGS.synchronous_snapshot_create:
+            clone_id = resp.Results['clone-id'][0]
+            clone_id_info = clone_id['clone-id-info'][0]
+            clone_operation_id = int(clone_id_info['clone-op-id'][0])
+
+            self._wait_for_clone_finished(clone_operation_id, host_id)
+        elif resp.Status == 'failed':
+            raise exception.CinderException(resp.Reason)
+
+    def _wait_for_clone_finished(self, clone_operation_id, host_id):
+        """
+        Polls ONTAP7 for clone status. Returns once clone is finished.
+        :param clone_operation_id: Identifier of ONTAP clone operation
+        """
+        clone_list_options = ('<clone-id>'
+                                '<clone-id-info>'
+                                  '<clone-op-id>%d</clone-op-id>'
+                                  '<volume-uuid></volume-uuid>'
+                                '</clone-id>'
+                              '</clone-id-info>')
+
+        request = self._client.factory.create('Request')
+        request.Name = 'clone-list-status'
+        request.Args = text.Raw(clone_list_options % clone_operation_id)
+
+        resp = self._client.service.ApiProxy(Target=host_id, Request=request)
+
+        while resp.Status != 'passed':
+            time.sleep(1)
+            resp = self._client.service.ApiProxy(Target=host_id,
+                                                Request=request)
+
+    def _get_provider_location(self, volume_id):
+        """
+        Returns provider location for given volume
+        :param volume_id:
+        """
+        volume = self.db.volume_get(self._context, volume_id)
+        return volume.provider_location
+
+    def _get_host_ip(self, volume_id):
+        """Returns IP address for the given volume"""
+        return self._get_provider_location(volume_id).split(':')[0]
+
+    def _get_export_path(self, volume_id):
+        """Returns NFS export path for the given volume"""
+        return self._get_provider_location(volume_id).split(':')[1]
+
+    def _get_host_id(self, volume_id):
+        """Returns ID of the ONTAP-7 host"""
+        host_ip = self._get_host_ip(volume_id)
+        server = self._client.service
+
+        resp = server.HostListInfoIterStart(ObjectNameOrId=host_ip)
+        tag = resp.Tag
+
+        try:
+            res = server.HostListInfoIterNext(Tag=tag, Maximum=1)
+            if hasattr(res, 'Hosts') and res.Hosts.HostInfo:
+                return res.Hosts.HostInfo[0].HostId
+        finally:
+            server.HostListInfoIterEnd(Tag=tag)
+
+    def _get_full_export_path(self, volume_id, host_id):
+        """Returns full path to the NFS share, e.g. /vol/vol0/home"""
+        export_path = self._get_export_path(volume_id)
+        command_args = '<pathname>%s</pathname>'
+
+        request = self._client.factory.create('Request')
+        request.Name = 'nfs-exportfs-storage-path'
+        request.Args = text.Raw(command_args % export_path)
+
+        resp = self._client.service.ApiProxy(Target=host_id,
+                                            Request=request)
+
+        if resp.Status == 'passed':
+            return resp.Results['actual-pathname'][0]
+        elif resp.Status == 'failed':
+            raise exception.CinderException(resp.Reason)
+
+    def _volume_not_present(self, nfs_mount, volume_name):
+        """
+        Check if volume exists
+        """
+        try:
+            self._try_execute('ls', self._get_volume_path(nfs_mount,
+                                                          volume_name))
+        except exception.ProcessExecutionError:
+            # If the volume isn't present
+            return True
+        return False
+
+    def _try_execute(self, *command, **kwargs):
+        # NOTE(vish): Volume commands can partially fail due to timing, but
+        #             running them a second time on failure will usually
+        #             recover nicely.
+        tries = 0
+        while True:
+            try:
+                self._execute(*command, **kwargs)
+                return True
+            except exception.ProcessExecutionError:
+                tries = tries + 1
+                if tries >= FLAGS.num_shell_tries:
+                    raise
+                LOG.exception(_("Recovering from a failed execute.  "
+                                "Try number %s"), tries)
+                time.sleep(tries ** 2)
+
+    def _get_volume_path(self, nfs_share, volume_name):
+        """Get volume path (local fs path) for given volume name on given nfs
+        share
+        @param nfs_share string, example 172.18.194.100:/var/nfs
+        @param volume_name string,
+            example volume-91ee65ec-c473-4391-8c09-162b00c68a8c
+        """
+        return os.path.join(self._get_mount_point_for_share(nfs_share),
+                            volume_name)
diff --git a/cinder/volume/nfs.py b/cinder/volume/nfs.py
new file mode 100644 (file)
index 0000000..5ea2367
--- /dev/null
@@ -0,0 +1,309 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright (c) 2012 NetApp, Inc.
+# All Rights Reserved.
+#
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
+#    not use this file except in compliance with the License. You may obtain
+#    a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#    License for the specific language governing permissions and limitations
+#    under the License.
+
+import os
+import errno
+import ctypes
+
+from cinder import flags
+from cinder.openstack.common import cfg
+from cinder.openstack.common import log as logging
+from cinder.volume import driver
+from cinder import exception
+
+LOG = logging.getLogger("cinder.volume.driver")
+
+volume_opts = [
+    cfg.StrOpt('nfs_shares_config',
+                default=None,
+                help='File with the list of available nfs shares'),
+    cfg.StrOpt('nfs_mount_point_base',
+               default='$state_path/mnt',
+               help='Base dir where nfs expected to be mounted'),
+    cfg.StrOpt('nfs_disk_util',
+               default='df',
+               help='Use du or df for free space calculation'),
+    cfg.BoolOpt('nfs_sparsed_volumes',
+                default=True,
+                help=('Create volumes as sparsed files which take no space.'
+                      'If set to False volume is created as regular file.'
+                      'In such case volume creation takes a lot of time.'))
+]
+
+FLAGS = flags.FLAGS
+FLAGS.register_opts(volume_opts)
+
+
+class NfsException(exception.CinderException):
+    pass
+
+
+class NfsNoSharesMounted(NfsException):
+    pass
+
+
+class NfsNoSuitableShareFound(NfsException):
+    pass
+
+
+class NfsDriver(driver.VolumeDriver):
+    """NFS based cinder driver. Creates file on NFS share for using it
+    as block device on hypervisor."""
+
+    def do_setup(self, context):
+        """Any initialization the volume driver does while starting"""
+        super(NfsDriver, self).do_setup(context)
+
+        config = FLAGS.nfs_shares_config
+        if not config:
+            LOG.warn(_("There's no NFS config file configured "))
+        if not config or not os.path.exists(config):
+            msg = _("NFS config file doesn't exist")
+            LOG.warn(msg)
+            raise NfsException(msg)
+
+        try:
+            self._execute('mount.nfs', check_exit_code=False)
+        except OSError as exc:
+            if exc.errno == errno.ENOENT:
+                raise NfsException('mount.nfs is not installed')
+            else:
+                raise
+
+    def check_for_setup_error(self):
+        """Just to override parent behavior"""
+        pass
+
+    def create_volume(self, volume):
+        """Creates a volume"""
+
+        self._ensure_shares_mounted()
+
+        volume['provider_location'] = self._find_share(volume['size'])
+
+        LOG.info(_('casted to %s') % volume['provider_location'])
+
+        self._do_create_volume(volume)
+
+        return {'provider_location': volume['provider_location']}
+
+    def delete_volume(self, volume):
+        """Deletes a logical volume."""
+
+        if not volume['provider_location']:
+            LOG.warn(_('Volume %s does not have provider_location specified, '
+                     'skipping'), volume['name'])
+            return True
+
+        self._ensure_share_mounted(volume['provider_location'])
+
+        mounted_path = self.local_path(volume)
+
+        if not self._path_exists(mounted_path):
+            volume = volume['name']
+
+            LOG.warn(_('Trying to delete non-existing volume %(volume)s at '
+                     'path %(mounted_path)s') % locals())
+            return True
+
+#        self._execute('dd', 'if=/dev/zero', 'of=%s' % mounted_volume_path,
+#                      'bs=1M', run_as_root=True)
+        self._execute('rm', '-f', mounted_path, run_as_root=True)
+
+    def ensure_export(self, ctx, volume):
+        """Synchronously recreates an export for a logical volume."""
+        self._ensure_share_mounted(volume['provider_location'])
+
+    def create_export(self, ctx, volume):
+        """Exports the volume. Can optionally return a Dictionary of changes
+        to the volume object to be persisted."""
+        pass
+
+    def remove_export(self, ctx, volume):
+        """Removes an export for a logical volume."""
+        pass
+
+    def check_for_export(self, context, volume_id):
+        """Make sure volume is exported."""
+        pass
+
+    def initialize_connection(self, volume, connector):
+        """Allow connection to connector and return connection info."""
+        data = {'export': volume['provider_location'],
+                'name': volume['name']}
+        return {
+            'driver_volume_type': 'nfs',
+            'data': data
+        }
+
+    def terminate_connection(self, volume, connector):
+        """Disallow connection from connector"""
+        pass
+
+    def local_path(self, volume):
+        """Get volume path (mounted locally fs path) for given volume
+        :param volume: volume reference
+        """
+        nfs_share = volume['provider_location']
+        return os.path.join(self._get_mount_point_for_share(nfs_share),
+                            volume['name'])
+
+    def _create_sparsed_file(self, path, size):
+        """Creates file with 0 disk usage"""
+        self._execute('truncate', '-s', self._sizestr(size),
+                      path, run_as_root=True)
+
+    def _create_regular_file(self, path, size):
+        """Creates regular file of given size. Takes a lot of time for large
+        files"""
+        KB = 1024
+        MB = KB * 1024
+        GB = MB * 1024
+
+        block_size_mb = 1
+        block_count = size * GB / (block_size_mb * MB)
+
+        self._execute('dd', 'if=/dev/zero', 'of=%s' % path,
+                      'bs=%dM' % block_size_mb,
+                      'count=%d' % block_count,
+                      run_as_root=True)
+
+    def _set_rw_permissions_for_all(self, path):
+        """Sets 666 permissions for the path"""
+        self._execute('chmod', 'ugo+rw', path, run_as_root=True)
+
+    def _do_create_volume(self, volume):
+        """Create a volume on given nfs_share
+        :param volume: volume reference
+        """
+        volume_path = self.local_path(volume)
+        volume_size = volume['size']
+
+        if FLAGS.nfs_sparsed_volumes:
+            self._create_sparsed_file(volume_path, volume_size)
+        else:
+            self._create_regular_file(volume_path, volume_size)
+
+        self._set_rw_permissions_for_all(volume_path)
+
+    def _ensure_shares_mounted(self):
+        """Look for NFS shares in the flags and tries to mount them locally"""
+        self._mounted_shares = []
+
+        for share in self._load_shares_config():
+            try:
+                self._ensure_share_mounted(share)
+                self._mounted_shares.append(share)
+            except Exception, exc:
+                LOG.warning('Exception during mounting %s' % (exc,))
+
+        LOG.debug('Available shares %s' % str(self._mounted_shares))
+
+    def _load_shares_config(self):
+        return [share.strip() for share in open(FLAGS.nfs_shares_config)
+                if share and not share.startswith('#')]
+
+    def _ensure_share_mounted(self, nfs_share):
+        """Mount NFS share
+        :param nfs_share:
+        """
+        mount_path = self._get_mount_point_for_share(nfs_share)
+        self._mount_nfs(nfs_share, mount_path, ensure=True)
+
+    def _find_share(self, volume_size_for):
+        """Choose NFS share among available ones for given volume size. Current
+        implementation looks for greatest capacity
+        :param volume_size_for: int size in Gb
+        """
+
+        if not self._mounted_shares:
+            raise NfsNoSharesMounted(
+                _("There is no any mounted NFS share found"))
+
+        greatest_size = 0
+        greatest_share = None
+
+        for nfs_share in self._mounted_shares:
+            capacity = self._get_available_capacity(nfs_share)
+            if capacity > greatest_size:
+                greatest_share = nfs_share
+                greatest_size = capacity
+
+        if volume_size_for * 1024 * 1024 * 1024 > greatest_size:
+            raise NfsNoSuitableShareFound(
+                _('There is no share which can host %sG') % volume_size_for)
+        return greatest_share
+
+    def _get_mount_point_for_share(self, nfs_share):
+        """
+        :param nfs_share: example 172.18.194.100:/var/nfs
+        """
+        return os.path.join(FLAGS.nfs_mount_point_base,
+                            self._get_hash_str(nfs_share))
+
+    def _get_available_capacity(self, nfs_share):
+        """Calculate available space on the NFS share
+        :param nfs_share: example 172.18.194.100:/var/nfs
+        """
+        mount_point = self._get_mount_point_for_share(nfs_share)
+
+        out, _ = self._execute('df', '-P', '-B', '1', mount_point,
+                               run_as_root=True)
+        out = out.splitlines()[1]
+
+        available = 0
+
+        if FLAGS.nfs_disk_util == 'df':
+            available = int(out.split()[3])
+        else:
+            size = int(out.split()[1])
+            out, _ = self._execute('du', '-sb', '--apparent-size',
+                                   '--exclude', '*snapshot*', mount_point,
+                                   run_as_root=True)
+            used = int(out.split()[0])
+            available = size - used
+
+        return available
+
+    def _mount_nfs(self, nfs_share, mount_path, ensure=False):
+        """Mount NFS share to mount path"""
+        if not self._path_exists(mount_path):
+            self._execute('mkdir', '-p', mount_path)
+
+        try:
+            self._execute('mount', '-t', 'nfs', nfs_share, mount_path,
+                          run_as_root=True)
+        except exception.ProcessExecutionError as exc:
+            if ensure and 'already mounted' in exc.stderr:
+                LOG.warn(_("%s is already mounted"), nfs_share)
+            else:
+                raise
+
+    def _path_exists(self, path):
+        """Check given path """
+        try:
+            self._execute('stat', path, run_as_root=True)
+            return True
+        except exception.ProcessExecutionError as exc:
+            if 'No such file or directory' in exc.stderr:
+                return False
+            else:
+                raise
+
+    def _get_hash_str(self, base_str):
+        """returns string that represents hash of base_str (in a hex format)"""
+        return str(ctypes.c_uint64(hash(base_str)).value)