Tintri Cinder Volume driver
* 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
This commit is contained in:
parent
c2db466995
commit
5467e73fb4
181
cinder/tests/unit/test_tintri.py
Normal file
181
cinder/tests/unit/test_tintri.py
Normal file
@ -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))
|
728
cinder/volume/drivers/tintri.py
Normal file
728
cinder/volume/drivers/tintri.py
Normal file
@ -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)
|
@ -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
|
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
|
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
|
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
|
||||||
|
Loading…
Reference in New Issue
Block a user