]> review.fuel-infra Code Review - openstack-build/cinder-build.git/commitdiff
Tintri Cinder Volume driver
authorSean Chen <schen@tintri.com>
Tue, 12 May 2015 21:59:30 +0000 (14:59 -0700)
committerSean Chen <xuchenx@gmail.com>
Tue, 2 Jun 2015 20:11:27 +0000 (13:11 -0700)
* Volume Create/Delete
* Volume Attach/Detach
* Snapshot Create/Delete
* Create Volume from Snapshot
* Get Volume Stats
* Copy Image to Volume
* Copy Volume to Image
* Clone Volume
* Extend Volume

Implements: blueprint tintri-cinder-driver

Change-Id: I4d9f2805e02524e0e271a263376b446565294d28

cinder/tests/unit/test_tintri.py [new file with mode: 0644]
cinder/volume/drivers/tintri.py [new file with mode: 0644]
etc/cinder/rootwrap.d/volume.filters

diff --git a/cinder/tests/unit/test_tintri.py b/cinder/tests/unit/test_tintri.py
new file mode 100644 (file)
index 0000000..25c2b5a
--- /dev/null
@@ -0,0 +1,181 @@
+# Copyright (c) 2015 Tintri.  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 test for Tintri storage.
+"""
+
+import mock
+
+from oslo_log import log as logging
+
+from cinder import context
+from cinder import exception
+from cinder import test
+from cinder.tests.unit import fake_snapshot
+from cinder.tests.unit import fake_volume
+from cinder.volume.drivers.tintri import TClient
+from cinder.volume.drivers.tintri import TintriDriver
+
+LOG = logging.getLogger(__name__)
+
+
+class FakeImage(object):
+    def __init__(self):
+        self.id = 'image-id'
+        self.name = 'image-name'
+
+    def __getitem__(self, key):
+        return self.__dict__[key]
+
+
+class TintriDriverTestCase(test.TestCase):
+    def setUp(self):
+        super(TintriDriverTestCase, self).setUp()
+        self.context = context.get_admin_context()
+        kwargs = {'configuration': self.create_configuration()}
+        self._driver = TintriDriver(**kwargs)
+        self._driver._hostname = 'host'
+        self._driver._username = 'user'
+        self._driver._password = 'password'
+        self._provider_location = 'host:/share'
+        self._driver._mounted_shares = [self._provider_location]
+        self.fake_stubs()
+
+    def create_configuration(self):
+        configuration = mock.Mock()
+        configuration.nfs_mount_point_base = '/mnt/test'
+        configuration.nfs_mount_options = None
+        configuration.nas_mount_options = None
+        return configuration
+
+    def fake_stubs(self):
+        self.stubs.Set(TClient, 'login', self.fake_login)
+        self.stubs.Set(TClient, 'logout', self.fake_logout)
+        self.stubs.Set(TClient, 'get_snapshot', self.fake_get_snapshot)
+        self.stubs.Set(TintriDriver, '_move_cloned_volume',
+                       self.fake_move_cloned_volume)
+        self.stubs.Set(TintriDriver, '_get_provider_location',
+                       self.fake_get_provider_location)
+        self.stubs.Set(TintriDriver, '_set_rw_permissions',
+                       self.fake_set_rw_permissions)
+        self.stubs.Set(TintriDriver, '_is_volume_present',
+                       self.fake_is_volume_present)
+        self.stubs.Set(TintriDriver, '_is_share_vol_compatible',
+                       self.fake_is_share_vol_compatible)
+        self.stubs.Set(TintriDriver, '_is_file_size_equal',
+                       self.fake_is_file_size_equal)
+
+    def fake_login(self, user_name, password):
+        return 'session-id'
+
+    def fake_logout(self):
+        pass
+
+    def fake_get_snapshot(self, volume_name):
+        return 'snapshot-id'
+
+    def fake_move_cloned_volume(self, clone_name, volume_id, share=None):
+        pass
+
+    def fake_get_provider_location(self, volume_path):
+        return self._provider_location
+
+    def fake_set_rw_permissions(self, path):
+        pass
+
+    def fake_is_volume_present(self, volume_path):
+        return True
+
+    def fake_is_share_vol_compatible(self, volume, share):
+        return True
+
+    def fake_is_file_size_equal(self, path, size):
+        return True
+
+    @mock.patch.object(TClient, 'create_snapshot', mock.Mock())
+    def test_create_snapshot(self):
+        snapshot = fake_snapshot.fake_snapshot_obj(self.context)
+        volume = fake_volume.fake_volume_obj(self.context)
+        snapshot.volume = volume
+        self._driver.create_snapshot(snapshot)
+
+    @mock.patch.object(TClient, 'create_snapshot', mock.Mock(
+                       side_effect=exception.VolumeDriverException))
+    def test_create_snapshot_failure(self):
+        snapshot = fake_snapshot.fake_snapshot_obj(self.context)
+        volume = fake_volume.fake_volume_obj(self.context)
+        snapshot.volume = volume
+        self.assertRaises(exception.VolumeDriverException,
+                          self._driver.create_snapshot, snapshot)
+
+    @mock.patch.object(TClient, 'delete_snapshot', mock.Mock())
+    def test_delete_snapshot(self):
+        snapshot = fake_snapshot.fake_snapshot_obj(self.context)
+        self._driver.delete_snapshot(snapshot)
+
+    @mock.patch.object(TClient, 'delete_snapshot', mock.Mock(
+                       side_effect=exception.VolumeDriverException))
+    def test_delete_snapshot_failure(self):
+        snapshot = fake_snapshot.fake_snapshot_obj(self.context)
+        self.assertRaises(exception.VolumeDriverException,
+                          self._driver.delete_snapshot, snapshot)
+
+    @mock.patch.object(TClient, 'clone_volume', mock.Mock())
+    def test_create_volume_from_snapshot(self):
+        snapshot = fake_snapshot.fake_snapshot_obj(self.context)
+        volume = fake_volume.fake_volume_obj(self.context)
+        self.assertEqual({'provider_location': self._provider_location},
+                         self._driver.create_volume_from_snapshot(
+                         volume, snapshot))
+
+    @mock.patch.object(TClient, 'clone_volume', mock.Mock(
+                       side_effect=exception.VolumeDriverException))
+    def test_create_volume_from_snapshot_failure(self):
+        snapshot = fake_snapshot.fake_snapshot_obj(self.context)
+        volume = fake_volume.fake_volume_obj(self.context)
+        self.assertRaises(exception.VolumeDriverException,
+                          self._driver.create_volume_from_snapshot,
+                          volume, snapshot)
+
+    @mock.patch.object(TClient, 'clone_volume', mock.Mock())
+    @mock.patch.object(TClient, 'create_snapshot', mock.Mock())
+    def test_create_cloned_volume(self):
+        volume = fake_volume.fake_volume_obj(self.context)
+        self.assertEqual({'provider_location': self._provider_location},
+                         self._driver.create_cloned_volume(volume, volume))
+
+    @mock.patch.object(TClient, 'clone_volume', mock.Mock(
+                       side_effect=exception.VolumeDriverException))
+    @mock.patch.object(TClient, 'create_snapshot', mock.Mock())
+    def test_create_cloned_volume_failure(self):
+        volume = fake_volume.fake_volume_obj(self.context)
+        self.assertRaises(exception.VolumeDriverException,
+                          self._driver.create_cloned_volume, volume, volume)
+
+    @mock.patch.object(TClient, 'clone_volume', mock.Mock())
+    def test_clone_image(self):
+        volume = fake_volume.fake_volume_obj(self.context)
+        self.assertEqual(({'provider_location': self._provider_location,
+                           'bootable': True}, True),
+                         self._driver.clone_image(
+                         None, volume, 'image-name', FakeImage(), None))
+
+    @mock.patch.object(TClient, 'clone_volume', mock.Mock(
+                       side_effect=exception.VolumeDriverException))
+    def test_clone_image_failure(self):
+        volume = fake_volume.fake_volume_obj(self.context)
+        self.assertEqual(({'provider_location': None,
+                           'bootable': False}, False),
+                         self._driver.clone_image(
+                         None, volume, 'image-name', FakeImage(), None))
diff --git a/cinder/volume/drivers/tintri.py b/cinder/volume/drivers/tintri.py
new file mode 100644 (file)
index 0000000..df31e06
--- /dev/null
@@ -0,0 +1,728 @@
+# Copyright (c) 2015 Tintri.  All rights reserved.
+# 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 Tintri storage.
+"""
+
+import json
+import os
+import re
+import socket
+import time
+
+from oslo_config import cfg
+from oslo_log import log as logging
+from oslo_utils import units
+import requests
+import six.moves.urllib.parse as urlparse
+
+from cinder import exception
+from cinder import utils
+from cinder.i18n import _, _LE, _LI, _LW
+from cinder.image import image_utils
+from cinder.volume.drivers import nfs
+
+LOG = logging.getLogger(__name__)
+api_version = 'v310'
+img_prefix = 'image-'
+tintri_path = '/tintri/'
+
+
+tintri_options = [
+    cfg.StrOpt('tintri_server_hostname',
+               default=None,
+               help='The hostname (or IP address) for the storage system'),
+    cfg.StrOpt('tintri_server_username',
+               default='admin',
+               help='User name for the storage system'),
+    cfg.StrOpt('tintri_server_password',
+               default=None,
+               help='Password for the storage system',
+               secret=True),
+]
+
+CONF = cfg.CONF
+CONF.register_opts(tintri_options)
+
+
+class TintriDriver(nfs.NfsDriver):
+    """Base class for Tintri driver."""
+
+    VENDOR = 'Tintri'
+    VERSION = '1.0.0'
+    REQUIRED_OPTIONS = ['tintri_server_hostname', 'tintri_server_password']
+
+    def __init__(self, *args, **kwargs):
+        self._execute = None
+        self._context = None
+        super(TintriDriver, self).__init__(*args, **kwargs)
+        self._execute_as_root = True
+        self.configuration.append_config_values(tintri_options)
+
+    def do_setup(self, context):
+        super(TintriDriver, self).do_setup(context)
+        self._context = context
+        self._check_ops(self.REQUIRED_OPTIONS, self.configuration)
+        self._hostname = getattr(self.configuration, 'tintri_server_hostname')
+        self._username = getattr(self.configuration, 'tintri_server_username',
+                                 CONF.tintri_server_username)
+        self._password = getattr(self.configuration, 'tintri_server_password')
+
+    def get_pool(self, volume):
+        """Returns pool name where volume resides.
+
+        :param volume: The volume hosted by the driver.
+        :return: Name of the pool where given volume is hosted.
+        """
+        return volume['provider_location']
+
+    def create_snapshot(self, snapshot):
+        """Creates a snapshot."""
+        self._create_volume_snapshot(snapshot.volume_name,
+                                     snapshot.name,
+                                     snapshot.volume_id,
+                                     vm_name=snapshot.display_name)
+
+    def delete_snapshot(self, snapshot):
+        """Deletes a snapshot."""
+        with TClient(self._hostname, self._username, self._password) as c:
+            snapshot_id = c.get_snapshot(snapshot.name)
+            if snapshot_id:
+                c.delete_snapshot(snapshot_id)
+
+    def _check_ops(self, required_ops, configuration):
+        """Ensures that the options we care about are set."""
+        for op in required_ops:
+            if not getattr(configuration, op):
+                LOG.error(_LE('Configuration value %s is not set.'), op)
+                raise exception.InvalidConfigurationValue(option=op,
+                                                          value=None)
+
+    def _create_volume_snapshot(self, volume_name, snapshot_name, volume_id,
+                                share=None, vm_name=None):
+        """Creates a volume snapshot."""
+        (__, path) = self._get_export_ip_path(volume_id, share)
+        volume_path = '%s/%s' % (path, volume_name)
+        with TClient(self._hostname, self._username, self._password) as c:
+            return c.create_snapshot(volume_path, snapshot_name, vm_name)
+
+    def create_volume_from_snapshot(self, volume, snapshot):
+        """Creates a volume from snapshot."""
+        vol_size = volume.size
+        snap_size = snapshot.volume_size
+
+        self._clone_volume(snapshot.name, volume.name, snapshot.volume_id)
+        share = self._get_provider_location(snapshot.volume_id)
+        volume['provider_location'] = share
+        path = self.local_path(volume)
+
+        self._set_rw_permissions(path)
+        if vol_size != snap_size:
+            try:
+                self.extend_volume(volume, vol_size)
+            except Exception:
+                LOG.error(_LE("Resizing %s failed. Cleaning volume."),
+                          volume.name)
+                self._delete_file(path)
+                raise
+
+        return {'provider_location': volume['provider_location']}
+
+    def _clone_volume(self, volume_name, clone_name, volume_id, share=None):
+        """Clones volume from snapshot."""
+        (host, path) = self._get_export_ip_path(volume_id, share)
+        clone_path = '%s/%s-d' % (path, clone_name)
+        with TClient(self._hostname, self._username, self._password) as c:
+            snapshot_id = c.get_snapshot(volume_name)
+            retry = 0
+            while not snapshot_id:
+                retry += 1
+                if retry > 5:
+                    msg = _('Failed to get snapshot for '
+                            'volume %s.') % volume_name
+                    raise exception.VolumeDriverException(msg)
+                time.sleep(1)
+                snapshot_id = c.get_snapshot(volume_name)
+            c.clone_volume(snapshot_id, clone_path)
+
+        self._move_cloned_volume(clone_name, volume_id, share)
+
+    def _clone_snapshot(self, snapshot_id, clone_name, volume_id, share=None):
+        """Clones volume from snapshot."""
+        (host, path) = self._get_export_ip_path(volume_id, share)
+        clone_path = '%s/%s-d' % (path, clone_name)
+        with TClient(self._hostname, self._username, self._password) as c:
+            c.clone_volume(snapshot_id, clone_path)
+
+        self._move_cloned_volume(clone_name, volume_id, share)
+
+    def _move_cloned_volume(self, clone_name, volume_id, share=None):
+        local_path = self._get_local_path(volume_id, share)
+        source_path = os.path.join(local_path, clone_name + '-d')
+        if self._is_volume_present(source_path):
+            source_file = os.listdir(source_path)[0]
+            source = os.path.join(source_path, source_file)
+            target = os.path.join(local_path, clone_name)
+            self._move_file(source, target)
+            self._execute('rm', '-rf', source_path,
+                          run_as_root=self._execute_as_root)
+        else:
+            raise exception.VolumeDriverException(
+                _("NFS file %s not discovered.") % source_path)
+
+    def _clone_volume_to_volume(self, volume_name, clone_name, volume_id,
+                                share=None, image_id=None, vm_name=None):
+        """Creates volume snapshot then clones volume."""
+        (host, path) = self._get_export_ip_path(volume_id, share)
+        volume_path = '%s/%s' % (path, volume_name)
+        clone_path = '%s/%s-d' % (path, clone_name)
+        with TClient(self._hostname, self._username, self._password) as c:
+            if share and image_id:
+                snapshot_id = self._create_image_snapshot(volume_name, share,
+                                                          image_id, vm_name)
+            else:
+                snapshot_id = c.create_snapshot(volume_path, volume_name,
+                                                vm_name)
+            c.clone_volume(snapshot_id, clone_path)
+
+        self._move_cloned_volume(clone_name, volume_id, share)
+
+    def _update_volume_stats(self):
+        """Retrieves stats info from volume group."""
+
+        data = {}
+        backend_name = self.configuration.safe_get('volume_backend_name')
+        data['volume_backend_name'] = backend_name or self.VENDOR
+        data['vendor_name'] = self.VENDOR
+        data['driver_version'] = self.get_version()
+        data['storage_protocol'] = self.driver_volume_type
+
+        self._ensure_shares_mounted()
+
+        global_capacity = 0
+        global_free = 0
+        for share in self._mounted_shares:
+            capacity, free, used = self._get_capacity_info(share)
+            global_capacity += capacity
+            global_free += free
+
+        data['total_capacity_gb'] = global_capacity / float(units.Gi)
+        data['free_capacity_gb'] = global_free / float(units.Gi)
+        data['reserved_percentage'] = 0
+        data['QoS_support'] = True
+        self._stats = data
+
+    def _get_provider_location(self, volume_id):
+        """Returns provider location for given volume."""
+        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 _resolve_hostname(self, hostname):
+        """Resolves host name to IP address."""
+        res = socket.getaddrinfo(hostname, None)[0]
+        family, socktype, proto, canonname, sockaddr = res
+        return sockaddr[0]
+
+    def _is_volume_present(self, volume_path):
+        """Checks if volume exists."""
+        try:
+            self._execute('ls', volume_path,
+                          run_as_root=self._execute_as_root)
+        except Exception:
+            return False
+        return True
+
+    def _get_volume_path(self, nfs_share, volume_name):
+        """Gets local volume path for given volume name on given nfs share."""
+        return os.path.join(self._get_mount_point_for_share(nfs_share),
+                            volume_name)
+
+    def create_cloned_volume(self, volume, src_vref):
+        """Creates a clone of the specified volume."""
+        vol_size = volume.size
+        src_vol_size = src_vref.size
+        self._clone_volume_to_volume(src_vref.name, volume.name, src_vref.id,
+                                     vm_name=src_vref.display_name)
+
+        share = self._get_provider_location(src_vref.id)
+        volume['provider_location'] = share
+        path = self.local_path(volume)
+
+        self._set_rw_permissions(path)
+        if vol_size != src_vol_size:
+            try:
+                self.extend_volume(volume, vol_size)
+            except Exception:
+                LOG.error(_LE("Resizing %s failed. Cleaning volume."),
+                          volume.name)
+                self._delete_file(path)
+                raise
+
+        return {'provider_location': volume['provider_location']}
+
+    def copy_image_to_volume(self, context, volume, image_service, image_id):
+        """Fetches the image from image_service and write it to the volume."""
+        super(TintriDriver, self).copy_image_to_volume(
+            context, volume, image_service, image_id)
+        LOG.info(_LI('Copied image to volume %s using regular download.'),
+                 volume['name'])
+        self._create_image_snapshot(volume['name'],
+                                    volume['provider_location'], image_id,
+                                    img_prefix + image_id)
+
+    def _create_image_snapshot(self, volume_name, share, image_id, image_name):
+        """Creates an image snapshot."""
+        snapshot_name = img_prefix + image_id
+        LOG.info(_LI("Creating image snapshot %s"), snapshot_name)
+        (host, path) = self._get_export_ip_path(None, share)
+        volume_path = '%s/%s' % (path, volume_name)
+
+        @utils.synchronized(snapshot_name, external=True)
+        def _do_snapshot():
+            with TClient(self._hostname, self._username, self._password) as c:
+                snapshot_id = c.get_snapshot(snapshot_name)
+                if not snapshot_id:
+                    snapshot_id = c.create_snapshot(volume_path, snapshot_name,
+                                                    image_name)
+                return snapshot_id
+
+        try:
+            return _do_snapshot()
+        except Exception as e:
+            LOG.warning(_LW('Exception while creating image %(image_id)s '
+                            'snapshot. Exception: %(exc)s'),
+                        {'image_id': image_id, 'exc': e})
+
+    def _find_image_snapshot(self, image_id):
+        """Finds image snapshot."""
+        snapshot_name = img_prefix + image_id
+        with TClient(self._hostname, self._username, self._password) as c:
+            return c.get_snapshot(snapshot_name)
+
+    def _clone_image_snapshot(self, snapshot_id, dst, share):
+        """Clones volume from image snapshot."""
+        file_path = self._get_volume_path(share, dst)
+        if not os.path.exists(file_path):
+            LOG.info(_LI('Cloning from snapshot to destination %s'), dst)
+            self._clone_snapshot(snapshot_id, dst, volume_id=None,
+                                 share=share)
+
+    def _delete_file(self, path):
+        """Deletes file from disk and return result as boolean."""
+        try:
+            LOG.debug('Deleting file at path %s', path)
+            cmd = ['rm', '-f', path]
+            self._execute(*cmd, run_as_root=self._execute_as_root)
+            return True
+        except Exception as e:
+            LOG.warning(_LW('Exception during deleting %s'), e)
+            return False
+
+    def _move_file(self, source_path, dest_path):
+        """Moves source to destination."""
+
+        @utils.synchronized(dest_path, external=True)
+        def _do_move(src, dst):
+            if os.path.exists(dst):
+                LOG.warning(_LW("Destination %s already exists."), dst)
+                return False
+            self._execute('mv', src, dst, run_as_root=self._execute_as_root)
+            return True
+
+        try:
+            return _do_move(source_path, dest_path)
+        except Exception as e:
+            LOG.warning(_LW('Exception moving file %(src)s. Message - %(e)s'),
+                        {'src': source_path, 'e': e})
+        return False
+
+    def clone_image(self, context, volume,
+                    image_location, image_meta,
+                    image_service):
+        """Creates a volume efficiently from an existing image.
+
+        image_location is a string whose format depends on the
+        image service backend in use. The driver should use it
+        to determine whether cloning is possible.
+
+        Returns a dict of volume properties eg. provider_location,
+        boolean indicating whether cloning occurred.
+        """
+        image_name = image_meta['name']
+        image_id = image_meta['id']
+        cloned = False
+        post_clone = False
+        try:
+            snapshot_id = self._find_image_snapshot(image_id)
+            if snapshot_id:
+                cloned = self._clone_from_snapshot(volume, image_id,
+                                                   snapshot_id)
+            else:
+                cloned = self._direct_clone(volume, image_location,
+                                            image_id, image_name)
+            if cloned:
+                post_clone = self._post_clone_image(volume)
+        except Exception as e:
+            LOG.info(_LI('Image cloning unsuccessful for image '
+                         '%(image_id)s. Message: %(msg)s'),
+                     {'image_id': image_id, 'msg': e})
+            vol_path = self.local_path(volume)
+            volume['provider_location'] = None
+            if os.path.exists(vol_path):
+                self._delete_file(vol_path)
+        finally:
+            cloned = cloned and post_clone
+            share = volume['provider_location'] if cloned else None
+            bootable = True if cloned else False
+            return {'provider_location': share, 'bootable': bootable}, cloned
+
+    def _clone_from_snapshot(self, volume, image_id, snapshot_id):
+        """Clones a copy from image snapshot."""
+        cloned = False
+        LOG.info(_LI('Cloning image %s from snapshot'), image_id)
+        for share in self._mounted_shares:
+            # Repeat tries in other shares if failed in some
+            LOG.debug('Image share: %s', share)
+            if (share and
+                    self._is_share_vol_compatible(volume, share)):
+                try:
+                    self._clone_image_snapshot(snapshot_id, volume['name'],
+                                               share)
+                    cloned = True
+                    volume['provider_location'] = share
+                    break
+                except Exception:
+                    LOG.warning(_LW('Unexpected exception during '
+                                    'image cloning in share %s'), share)
+        return cloned
+
+    def _direct_clone(self, volume, image_location, image_id, image_name):
+        """Clones directly in nfs share."""
+        LOG.info(_LI('Checking image clone %s from glance share.'), image_id)
+        cloned = False
+        image_location = self._get_image_nfs_url(image_location)
+        share = self._is_cloneable_share(image_location)
+        run_as_root = self._execute_as_root
+
+        if share and self._is_share_vol_compatible(volume, share):
+            LOG.debug('Share is cloneable %s', share)
+            volume['provider_location'] = share
+            (__, ___, img_file) = image_location.rpartition('/')
+            dir_path = self._get_mount_point_for_share(share)
+            img_path = '%s/%s' % (dir_path, img_file)
+            img_info = image_utils.qemu_img_info(img_path,
+                                                 run_as_root=run_as_root)
+            if img_info.file_format == 'raw':
+                LOG.debug('Image is raw %s', image_id)
+                self._clone_volume_to_volume(
+                    img_file, volume['name'],
+                    volume_id=None, share=share,
+                    image_id=image_id, vm_name=image_name)
+                cloned = True
+            else:
+                LOG.info(_LI('Image will locally be converted to raw %s'),
+                         image_id)
+                dst = '%s/%s' % (dir_path, volume['name'])
+                image_utils.convert_image(img_path, dst, 'raw',
+                                          run_as_root=run_as_root)
+                data = image_utils.qemu_img_info(dst, run_as_root=run_as_root)
+                if data.file_format != "raw":
+                    raise exception.InvalidResults(
+                        _("Converted to raw, but"
+                          " format is now %s") % data.file_format)
+                else:
+                    cloned = True
+                    self._create_image_snapshot(
+                        volume['name'], volume['provider_location'],
+                        image_id, image_name)
+        return cloned
+
+    def _post_clone_image(self, volume):
+        """Performs operations post image cloning."""
+        LOG.info(_LI('Performing post clone for %s'), volume['name'])
+        vol_path = self.local_path(volume)
+        self._set_rw_permissions(vol_path)
+        self._resize_image_file(vol_path, volume['size'])
+        return True
+
+    def _resize_image_file(self, path, new_size):
+        """Resizes the image file on share to new size."""
+        LOG.debug('Checking file for resize.')
+        if self._is_file_size_equal(path, new_size):
+            return
+        else:
+            LOG.info(_LI('Resizing file to %sG'), new_size)
+            image_utils.resize_image(path, new_size,
+                                     run_as_root=self._execute_as_root)
+            if self._is_file_size_equal(path, new_size):
+                return
+            else:
+                raise exception.InvalidResults(
+                    _('Resizing image file failed.'))
+
+    def _is_cloneable_share(self, image_location):
+        """Finds if the image at location is cloneable."""
+        conn, dr = self._check_nfs_path(image_location)
+        return self._is_share_in_use(conn, dr)
+
+    def _check_nfs_path(self, image_location):
+        """Checks if the nfs path format is matched.
+
+        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\-\.]+:[\d]+|[\w\-\.]+)(/[^/].*)*(/[^/\\\\]+))$'
+            matched = re.match(nfs_loc_pattern, image_location)
+            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 _is_share_in_use(self, conn, dr):
+        """Checks if share is cinder mounted and returns it."""
+        try:
+            if conn:
+                host = conn.split(':')[0]
+                ip = self._resolve_hostname(host)
+                for sh in self._mounted_shares:
+                    sh_ip = self._resolve_hostname(sh.split(':')[0])
+                    sh_exp = sh.split(':')[1]
+                    if sh_ip == ip and sh_exp == dr:
+                        LOG.debug('Found share match %s', sh)
+                        return sh
+        except Exception:
+            LOG.warning(_LW("Unexpected exception while "
+                            "short listing used share."))
+
+    def _get_image_nfs_url(self, image_location):
+        """Gets direct url for nfs backend.
+
+        It creates direct url from image_location
+        which is a tuple with direct_url and locations.
+        Returns url with nfs scheme if nfs store else returns url.
+        It needs to be verified by backend before use.
+        """
+
+        direct_url, locations = image_location
+        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
+        if not locations:
+            return direct_url
+        location = locations[0]
+        url = location['url']
+        if not location['metadata']:
+            return url
+        location_type = location['metadata'].get('type')
+        if not location_type or location_type.lower() != "nfs":
+            return url
+        share_location = location['metadata'].get('share_location')
+        mount_point = location['metadata'].get('mount_point')
+        if not share_location or not mount_point:
+            return url
+        url_parse = urlparse.urlparse(url)
+        abs_path = os.path.join(url_parse.netloc, url_parse.path)
+        rel_path = os.path.relpath(abs_path, mount_point)
+        direct_url = "%s/%s" % (share_location, rel_path)
+        return direct_url
+
+    def _is_share_vol_compatible(self, volume, share):
+        """Checks if share is compatible with volume to host it."""
+        return self._is_share_eligible(share, volume['size'])
+
+    def _can_share_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 _get_export_ip_path(self, volume_id=None, share=None):
+        """Returns export ip and path.
+
+          One of volume id or share is used to return the values.
+        """
+
+        if volume_id:
+            host_ip = self._get_host_ip(volume_id)
+            export_path = self._get_export_path(volume_id)
+        elif share:
+            host_ip = share.split(':')[0]
+            export_path = share.split(':')[1]
+        else:
+            raise exception.InvalidInput(
+                reason=_('A volume ID or share was not specified.'))
+        return host_ip, export_path
+
+    def _get_local_path(self, volume_id=None, share=None):
+        """Returns local path.
+
+          One of volume id or share is used to return the values.
+        """
+
+        if volume_id:
+            local_path = self._get_mount_point_for_share(
+                self._get_provider_location(volume_id))
+        elif share:
+            local_path = self._get_mount_point_for_share(share)
+        else:
+            raise exception.InvalidInput(
+                reason=_('A volume ID or share was not specified.'))
+        return local_path
+
+
+class TClient(object):
+    """REST client for Tintri"""
+
+    def __init__(self, hostname, username, password):
+        """Initializes a connection to Tintri server."""
+        self.api_url = 'https://' + hostname + '/api'
+        self.session_id = self.login(username, password)
+        self.headers = {'content-type': 'application/json',
+                        'cookie': 'JSESSIONID=' + self.session_id}
+
+    def __enter__(self):
+        return self
+
+    def __exit__(self, type, value, traceback):
+        self.logout()
+
+    def get(self, api):
+        return self.get_query(api, None)
+
+    def get_query(self, api, query):
+        url = self.api_url + api
+
+        return requests.get(url, headers=self.headers,
+                            params=query, verify=False)
+
+    def delete(self, api):
+        url = self.api_url + api
+
+        return requests.delete(url, headers=self.headers, verify=False)
+
+    def put(self, api, payload):
+        url = self.api_url + api
+
+        return requests.put(url, data=json.dumps(payload),
+                            headers=self.headers, verify=False)
+
+    def post(self, api, payload):
+        url = self.api_url + api
+
+        return requests.post(url, data=json.dumps(payload),
+                             headers=self.headers, verify=False)
+
+    def login(self, username, password):
+        # Payload, header and URL for login
+        headers = {'content-type': 'application/json'}
+        payload = {'username': username,
+                   'password': password,
+                   'typeId': 'com.tintri.api.rest.vcommon.dto.rbac.'
+                             'RestApiCredentials'}
+        url = self.api_url + '/' + api_version + '/session/login'
+
+        r = requests.post(url, data=json.dumps(payload),
+                          headers=headers, verify=False)
+
+        if r.status_code != 200:
+            msg = _('Failed to login for user %s.') % username
+            raise exception.VolumeDriverException(msg)
+
+        return r.cookies['JSESSIONID']
+
+    def logout(self):
+        url = self.api_url + '/' + api_version + '/session/logout'
+
+        requests.get(url, headers=self.headers, verify=False)
+
+    @staticmethod
+    def _remove_prefix(volume_path, prefix):
+        if volume_path.startswith(prefix):
+            return volume_path[len(prefix):]
+        else:
+            return volume_path
+
+    def create_snapshot(self, volume_path, volume_name, vm_name):
+        """Creates a volume snapshot."""
+        request = {'typeId': 'com.tintri.api.rest.' + api_version +
+                             '.dto.domain.beans.cinder.CinderSnapshotSpec',
+                   'file': TClient._remove_prefix(volume_path, tintri_path),
+                   'vmName': vm_name or volume_name,
+                   'description': 'Cinder ' + volume_name,
+                   'vmTintriUuid': volume_name,
+                   'instanceId': volume_name,
+                   'snapshotCreator': 'Cinder'
+                   }
+
+        payload = '/' + api_version + '/cinder/snapshot'
+        r = self.post(payload, request)
+        if r.status_code != 200:
+            msg = _('Failed to create snapshot for volume %s.') % volume_path
+            raise exception.VolumeDriverException(msg)
+
+        return r.json()[0]
+
+    def get_snapshot(self, volume_name):
+        """Gets a volume snapshot."""
+        filter = {'vmUuid': volume_name}
+
+        payload = '/' + api_version + '/snapshot'
+        r = self.get_query(payload, filter)
+        if r.status_code != 200:
+            msg = _('Failed to get snapshot for volume %s.') % volume_name
+            raise exception.VolumeDriverException(msg)
+
+        if int(r.json()['filteredTotal']) > 0:
+            return r.json()['items'][0]['uuid']['uuid']
+
+    def delete_snapshot(self, snapshot_uuid):
+        """Deletes a snapshot."""
+        url = '/' + api_version + '/snapshot/'
+        self.delete(url + snapshot_uuid)
+
+    def clone_volume(self, snapshot_uuid, volume_path):
+        """Clones a volume from snapshot."""
+        request = {'typeId': 'com.tintri.api.rest.' + api_version +
+                             '.dto.domain.beans.cinder.CinderCloneSpec',
+                   'destinationPaths':
+                       [TClient._remove_prefix(volume_path, tintri_path)],
+                   'tintriSnapshotUuid': snapshot_uuid
+                   }
+
+        url = '/' + api_version + '/cinder/clone'
+        r = self.post(url, request)
+        if r.status_code != 200 and r.status_code != 204:
+            msg = _('Failed to clone volume from snapshot %s.') % snapshot_uuid
+            raise exception.VolumeDriverException(msg)
index 36a037bb97b578898f8d97f92f07c381e9ef7a41..a00cdcc4d5ee651ea5494f2505678675fdb6ec34 100644 (file)
@@ -185,3 +185,6 @@ auiscsi: EnvFilter, env, root, LANG=, STONAVM_HOME=, LD_LIBRARY_PATH=, STONAVM_R
 audppool: EnvFilter, env, root, LANG=, STONAVM_HOME=, LD_LIBRARY_PATH=, STONAVM_RSP_PASS=, STONAVM_ACT=, /usr/stonavm/audppool
 aureplicationlocal: EnvFilter, env, root, LANG=, STONAVM_HOME=, LD_LIBRARY_PATH=, STONAVM_RSP_PASS=, STONAVM_ACT=, /usr/stonavm/aureplicationlocal
 aureplicationmon: EnvFilter, env, root, LANG=, STONAVM_HOME=, LD_LIBRARY_PATH=, STONAVM_RSP_PASS=, STONAVM_ACT=, /usr/stonavm/aureplicationmon
+
+# cinder/volume/drivers/tintri.py
+mv: CommandFilter, mv, root