Nexenta: Add NBD driver for NexentaEdge.

DocImpact
Implements: blueprint nexentaedge-ndb-driver

Change-Id: Iead87f00d75bfa93a06c7d2c1c6ab708bc5d804b
This commit is contained in:
Aleksey Ruban 2016-05-23 11:00:33 -06:00
parent 45d0f6270c
commit ca9e590f82
5 changed files with 882 additions and 6 deletions

View File

@ -0,0 +1,518 @@
# Copyright 2016 Nexenta Systems, 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 base64
import socket
import mock
from mock import patch
from oslo_serialization import jsonutils
from oslo_utils import units
from cinder import context
from cinder import exception
from cinder import test
from cinder.volume import configuration as conf
from cinder.volume.drivers.nexenta.nexentaedge import nbd
class FakeResponse(object):
def __init__(self, response):
self.response = response
super(FakeResponse, self).__init__()
def json(self):
return self.response
def close(self):
pass
class RequestParams(object):
def __init__(self, scheme, host, port, user, password):
self.scheme = scheme.lower()
self.host = host
self.port = port
self.user = user
self.password = password
def url(self, path=''):
return '%s://%s:%s/%s' % (
self.scheme, self.host, self.port, path)
@property
def headers(self):
auth = base64.b64encode(
('%s:%s' % (self.user, self.password)).encode('utf-8'))
headers = {
'Content-Type': 'application/json',
'Authorization': 'Basic %s' % auth
}
return headers
def build_post_args(self, args):
return jsonutils.dumps(args)
class TestNexentaEdgeNBDDriver(test.TestCase):
def setUp(self):
def _safe_get(opt):
return getattr(self.cfg, opt)
super(TestNexentaEdgeNBDDriver, self).setUp()
self.cfg = mock.Mock(spec=conf.Configuration)
self.cfg.safe_get = mock.Mock(side_effect=_safe_get)
self.cfg.trace_flags = 'fake_trace_flags'
self.cfg.driver_data_namespace = 'fake_driver_data_namespace'
self.cfg.nexenta_rest_protocol = 'http'
self.cfg.nexenta_rest_address = '127.0.0.1'
self.cfg.nexenta_rest_port = 8080
self.cfg.nexenta_rest_user = 'admin'
self.cfg.nexenta_rest_password = '0'
self.cfg.nexenta_lun_container = 'cluster/tenant/bucket'
self.cfg.nexenta_nbd_symlinks_dir = '/dev/disk/by-path'
self.cfg.volume_dd_blocksize = 512
self.cfg.nexenta_blocksize = 512
self.cfg.nexenta_chunksize = 4096
self.cfg.reserved_percentage = 0
self.ctx = context.get_admin_context()
self.drv = nbd.NexentaEdgeNBDDriver(configuration=self.cfg)
self.drv.do_setup(self.ctx)
self.request_params = RequestParams(
'http', self.cfg.nexenta_rest_address, self.cfg.nexenta_rest_port,
self.cfg.nexenta_rest_user, self.cfg.nexenta_rest_password)
def test_check_do_setup__symlinks_dir_not_specified(self):
self.drv.symlinks_dir = None
self.assertRaises(
exception.NexentaException, self.drv.check_for_setup_error)
def test_check_do_setup__symlinks_dir_doesnt_exist(self):
self.drv.symlinks_dir = '/some/random/path'
self.assertRaises(
exception.NexentaException, self.drv.check_for_setup_error)
@patch('requests.get')
@patch('os.path.exists')
def test_check_do_setup__empty_response(self, exists, get):
get.return_value = FakeResponse({})
exists.return_value = True
self.assertRaises(exception.VolumeBackendAPIException,
self.drv.check_for_setup_error)
@patch('requests.get')
@patch('os.path.exists')
def test_check_do_setup(self, exists, get):
get.return_value = FakeResponse({'response': 'OK'})
exists.return_value = True
self.drv.check_for_setup_error()
get.assert_any_call(
self.request_params.url(self.drv.bucket_url + '/objects/'),
headers=self.request_params.headers)
def test_local_path__error(self):
self.drv._get_nbd_number = lambda volume_: -1
volume = {'name': 'volume'}
self.assertRaises(exception.VolumeBackendAPIException,
self.drv.local_path, volume)
@patch('requests.get')
def test_local_path(self, get):
volume = {
'name': 'volume',
'host': 'myhost@backend#pool'
}
_get_host_info__response = {
'stats': {
'servers': {
'host1': {
'hostname': 'host1',
'ipv6addr': 'fe80::fc16:3eff:fedb:bd69'},
'host2': {
'hostname': 'myhost',
'ipv6addr': 'fe80::fc16:3eff:fedb:bd68'}
}
}
}
_get_nbd_devices__response = {
'value': jsonutils.dumps([
{
'objectPath': '/'.join(
(self.cfg.nexenta_lun_container, 'some_volume')),
'number': 1
},
{
'objectPath': '/'.join(
(self.cfg.nexenta_lun_container, volume['name'])),
'number': 2
}
])
}
def my_side_effect(*args, **kwargs):
if args[0] == self.request_params.url('system/stats'):
return FakeResponse({'response': _get_host_info__response})
elif args[0].startswith(
self.request_params.url('sysconfig/nbd/devices')):
return FakeResponse({'response': _get_nbd_devices__response})
else:
raise Exception('Unexpected request')
get.side_effect = my_side_effect
self.drv.local_path(volume)
@patch('requests.get')
def test_local_path__host_not_found(self, get):
volume = {
'name': 'volume',
'host': 'unknown-host@backend#pool'
}
_get_host_info__response = {
'stats': {
'servers': {
'host1': {
'hostname': 'host1',
'ipv6addr': 'fe80::fc16:3eff:fedb:bd69'},
'host2': {
'hostname': 'myhost',
'ipv6addr': 'fe80::fc16:3eff:fedb:bd68'}
}
}
}
_get_nbd_devices__response = {
'value': jsonutils.dumps([
{
'objectPath': '/'.join(
(self.cfg.nexenta_lun_container, 'some_volume')),
'number': 1
},
{
'objectPath': '/'.join(
(self.cfg.nexenta_lun_container, volume['name'])),
'number': 2
}
])
}
def my_side_effect(*args, **kwargs):
if args[0] == self.request_params.url('system/stats'):
return FakeResponse({'response': _get_host_info__response})
elif args[0].startswith(
self.request_params.url('sysconfig/nbd/devices')):
return FakeResponse({'response': _get_nbd_devices__response})
else:
raise Exception('Unexpected request')
get.side_effect = my_side_effect
self.assertRaises(exception.VolumeBackendAPIException,
self.drv.local_path, volume)
@patch('cinder.utils.execute')
@patch('requests.post')
def test_create_volume(self, post, execute):
post.returning_value = FakeResponse({})
volume = {
'host': 'host@backend#pool info',
'size': 1,
'name': 'volume'
}
number = 5
remote_url = ''
self.drv._get_remote_url = lambda host_: remote_url
self.drv._get_nbd_number = lambda volume_: number
self.drv.create_volume(volume)
post.assert_called_with(
self.request_params.url('nbd' + remote_url),
data=self.request_params.build_post_args({
'objectPath': '/'.join((self.cfg.nexenta_lun_container,
volume['name'])),
'volSizeMB': volume['size'] * units.Ki,
'blockSize': self.cfg.nexenta_blocksize,
'chunkSize': self.cfg.nexenta_chunksize}),
headers=self.request_params.headers)
@patch('requests.delete')
def test_delete_volume(self, delete):
delete.returning_value = FakeResponse({})
volume = {
'host': 'host@backend#pool info',
'size': 1,
'name': 'volume'
}
number = 5
remote_url = ''
self.drv._get_remote_url = lambda host_: remote_url
self.drv._get_nbd_number = lambda volume_: number
self.drv.delete_volume(volume)
delete.assert_called_with(
self.request_params.url('nbd' + remote_url),
data=self.request_params.build_post_args({
'objectPath': '/'.join((self.cfg.nexenta_lun_container,
volume['name'])),
'number': number}),
headers=self.request_params.headers)
@patch('requests.delete')
def test_delete_volume__not_found(self, delete):
delete.returning_value = FakeResponse({})
volume = {
'host': 'host@backend#pool info',
'size': 1,
'name': 'volume'
}
remote_url = ''
self.drv._get_remote_url = lambda host_: remote_url
self.drv._get_nbd_number = lambda volume_: -1
self.drv.delete_volume(volume)
delete.assert_not_called()
@patch('requests.put')
def test_extend_volume(self, put):
put.returning_value = FakeResponse({})
volume = {
'host': 'host@backend#pool info',
'size': 1,
'name': 'volume'
}
new_size = 5
remote_url = ''
self.drv._get_remote_url = lambda host_: remote_url
self.drv.extend_volume(volume, new_size)
put.assert_called_with(
self.request_params.url('nbd/resize' + remote_url),
data=self.request_params.build_post_args({
'objectPath': '/'.join((self.cfg.nexenta_lun_container,
volume['name'])),
'newSizeMB': new_size * units.Ki}),
headers=self.request_params.headers)
@patch('requests.post')
def test_create_snapshot(self, post):
post.returning_value = FakeResponse({})
snapshot = {
'name': 'dsfsdsdgfdf',
'volume_name': 'volume'
}
self.drv.create_snapshot(snapshot)
post.assert_called_with(
self.request_params.url('nbd/snapshot'),
data=self.request_params.build_post_args({
'objectPath': '/'.join((self.cfg.nexenta_lun_container,
snapshot['volume_name'])),
'snapName': snapshot['name']}),
headers=self.request_params.headers)
@patch('requests.delete')
def test_delete_snapshot(self, delete):
delete.returning_value = FakeResponse({})
snapshot = {
'name': 'dsfsdsdgfdf',
'volume_name': 'volume'
}
self.drv.delete_snapshot(snapshot)
delete.assert_called_with(
self.request_params.url('nbd/snapshot'),
data=self.request_params.build_post_args({
'objectPath': '/'.join((self.cfg.nexenta_lun_container,
snapshot['volume_name'])),
'snapName': snapshot['name']}),
headers=self.request_params.headers)
@patch('requests.put')
def test_create_volume_from_snapshot(self, put):
put.returning_value = FakeResponse({})
snapshot = {
'name': 'dsfsdsdgfdf',
'volume_size': 1,
'volume_name': 'volume'
}
volume = {
'host': 'host@backend#pool info',
'size': 2,
'name': 'volume'
}
remote_url = ''
self.drv._get_remote_url = lambda host_: remote_url
self.drv.extend_volume = lambda v, s: None
self.drv.create_volume_from_snapshot(volume, snapshot)
put.assert_called_with(
self.request_params.url('nbd/snapshot/clone' + remote_url),
data=self.request_params.build_post_args({
'objectPath': '/'.join((self.cfg.nexenta_lun_container,
snapshot['volume_name'])),
'snapName': snapshot['name'],
'clonePath': '/'.join((self.cfg.nexenta_lun_container,
volume['name']))
}),
headers=self.request_params.headers)
@patch('requests.post')
def test_create_cloned_volume(self, post):
post.returning_value = FakeResponse({})
volume = {
'host': 'host@backend#pool info',
'size': 1,
'name': 'volume'
}
src_vref = {
'size': 1,
'name': 'qwerty'
}
container = self.cfg.nexenta_lun_container
remote_url = ''
self.drv._get_remote_url = lambda host_: remote_url
self.drv.create_cloned_volume(volume, src_vref)
post.assert_called_with(
self.request_params.url('nbd' + remote_url),
data=self.request_params.build_post_args({
'objectPath': '/'.join((container, volume['name'])),
'volSizeMB': src_vref['size'] * units.Ki,
'blockSize': self.cfg.nexenta_blocksize,
'chunkSize': self.cfg.nexenta_chunksize
}),
headers=self.request_params.headers)
@patch('requests.post')
def test_create_cloned_volume_gt_src(self, post):
post.returning_value = FakeResponse({})
volume = {
'host': 'host@backend#pool info',
'size': 2,
'name': 'volume'
}
src_vref = {
'size': 1,
'name': 'qwerty'
}
container = self.cfg.nexenta_lun_container
remote_url = ''
self.drv._get_remote_url = lambda host_: remote_url
self.drv.create_cloned_volume(volume, src_vref)
post.assert_called_with(
self.request_params.url('nbd' + remote_url),
data=self.request_params.build_post_args({
'objectPath': '/'.join((container, volume['name'])),
'volSizeMB': volume['size'] * units.Ki,
'blockSize': self.cfg.nexenta_blocksize,
'chunkSize': self.cfg.nexenta_chunksize
}),
headers=self.request_params.headers)
@patch('requests.get')
def test_get_volume_stats(self, get):
self.cfg.volume_backend_name = None
get.return_value = FakeResponse({
'response': {
'stats': {
'summary': {
'total_capacity': units.Gi,
'total_available': units.Gi
}
}
}
})
location_info = '%(driver)s:%(host)s:%(bucket)s' % {
'driver': self.drv.__class__.__name__,
'host': socket.gethostname(),
'bucket': self.cfg.nexenta_lun_container
}
expected = {
'vendor_name': 'Nexenta',
'driver_version': self.drv.VERSION,
'storage_protocol': 'NBD',
'reserved_percentage': self.cfg.reserved_percentage,
'total_capacity_gb': 1,
'free_capacity_gb': 1,
'QoS_support': False,
'volume_backend_name': self.drv.__class__.__name__,
'location_info': location_info,
'restapi_url': self.request_params.url()
}
self.assertEqual(expected, self.drv.get_volume_stats())
@patch('cinder.image.image_utils.fetch_to_raw')
def test_copy_image_to_volume(self, fetch_to_raw):
volume = {
'host': 'host@backend#pool info',
'size': 1,
'name': 'volume'
}
self.drv.local_path = lambda host: 'local_path'
self.drv.copy_image_to_volume(self.ctx, volume, 'image_service',
'image_id')
fetch_to_raw.assert_called_with(
self.ctx, 'image_service', 'image_id', 'local_path',
self.cfg.volume_dd_blocksize, size=volume['size'])
@patch('cinder.image.image_utils.upload_volume')
def test_copy_volume_to_image(self, upload_volume):
volume = {
'host': 'host@backend#pool info',
'size': 1,
'name': 'volume'
}
self.drv.local_path = lambda host: 'local_path'
self.drv.copy_volume_to_image(self.ctx, volume, 'image_service',
'image_meta')
upload_volume.assert_called_with(
self.ctx, 'image_service', 'image_meta', 'local_path')
@patch('requests.get')
def test_validate_connector(self, get):
connector = {'host': 'host2'}
r = {
'stats': {
'servers': {
'host1': {'hostname': 'host1'},
'host2': {'hostname': 'host2'}
}
}
}
get.return_value = FakeResponse({'response': r})
self.drv.validate_connector(connector)
get.assert_called_with(self.request_params.url('system/stats'),
headers=self.request_params.headers)
@patch('requests.get')
def test_validate_connector__host_not_found(self, get):
connector = {'host': 'host3'}
r = {
'stats': {
'servers': {
'host1': {'hostname': 'host1'},
'host2': {'hostname': 'host2'}
}
}
}
get.return_value = FakeResponse({'response': r})
self.assertRaises(exception.VolumeBackendAPIException,
self.drv.validate_connector, connector)
def test_initialize_connection(self):
connector = {'host': 'host'}
volume = {
'host': 'host@backend#pool info',
'size': 1,
'name': 'volume'
}
self.drv.local_path = lambda host: 'local_path'
self.assertEqual({
'driver_volume_type': 'local',
'data': {'device_path': 'local_path'}},
self.drv.initialize_connection(volume, connector))

View File

@ -13,6 +13,7 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
import base64
import json import json
import requests import requests
import socket import socket
@ -73,22 +74,27 @@ class NexentaEdgeJSONProxy(object):
if len(args) > 1: if len(args) > 1:
data = json.dumps(args[1]) data = json.dumps(args[1])
auth = ('%s:%s' % (self.user, self.password)).encode('base64')[:-1] auth = base64.b64encode(
('%s:%s' % (self.user, self.password)).encode('utf-8'))
headers = { headers = {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Authorization': 'Basic %s' % auth 'Authorization': 'Basic %s' % auth
} }
LOG.debug('Sending JSON data: %s, data: %s', self.url, data) LOG.debug('Sending JSON data: %s, method: %s, data: %s', self.url,
self.method, data)
if self.method == 'get': if self.method == 'get':
req = requests.get(self.url, headers=headers) req = requests.get(self.url, headers=headers)
if self.method == 'post': elif self.method == 'post':
req = requests.post(self.url, data=data, headers=headers) req = requests.post(self.url, data=data, headers=headers)
if self.method == 'put': elif self.method == 'put':
req = requests.put(self.url, data=data, headers=headers) req = requests.put(self.url, data=data, headers=headers)
if self.method == 'delete': elif self.method == 'delete':
req = requests.delete(self.url, data=data, headers=headers) req = requests.delete(self.url, data=data, headers=headers)
else:
raise exception.VolumeDriverException(
message=_('Unsupported method: %s') % self.method)
rsp = req.json() rsp = req.json()
req.close() req.close()
@ -96,5 +102,5 @@ class NexentaEdgeJSONProxy(object):
LOG.debug('Got response: %s', rsp) LOG.debug('Got response: %s', rsp)
if rsp.get('response') is None: if rsp.get('response') is None:
raise exception.VolumeBackendAPIException( raise exception.VolumeBackendAPIException(
_('Error response: %s') % rsp) data=_('Error response: %s') % rsp)
return rsp.get('response') return rsp.get('response')

View File

@ -0,0 +1,346 @@
# Copyright 2016 Nexenta Systems, 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 json
import os
import six
import socket
from oslo_log import log as logging
from oslo_utils import excutils
from oslo_utils import units
from cinder import exception
from cinder.i18n import _, _LE, _LI
from cinder.image import image_utils
from cinder import interface
from cinder import utils as cinder_utils
from cinder.volume import driver
from cinder.volume.drivers.nexenta.nexentaedge import jsonrpc
from cinder.volume.drivers.nexenta import options
from cinder.volume.drivers.nexenta import utils as nexenta_utils
from cinder.volume import utils as volutils
LOG = logging.getLogger(__name__)
@interface.volumedriver
class NexentaEdgeNBDDriver(driver.VolumeDriver):
"""Executes commands relating to NBD Volumes.
Version history:
1.0.0 - Initial driver version.
"""
VERSION = '1.0.0'
def __init__(self, vg_obj=None, *args, **kwargs):
LOG.debug('NexentaEdgeNBDDriver. Trying to initialize.')
super(NexentaEdgeNBDDriver, self).__init__(*args, **kwargs)
if self.configuration:
self.configuration.append_config_values(
options.NEXENTA_CONNECTION_OPTS)
self.configuration.append_config_values(
options.NEXENTA_DATASET_OPTS)
self.configuration.append_config_values(
options.NEXENTA_EDGE_OPTS)
self.restapi_protocol = self.configuration.nexenta_rest_protocol
self.restapi_host = self.configuration.nexenta_rest_address
self.restapi_port = self.configuration.nexenta_rest_port
self.restapi_user = self.configuration.nexenta_rest_user
self.restapi_password = self.configuration.nexenta_rest_password
self.bucket_path = self.configuration.nexenta_lun_container
self.blocksize = self.configuration.nexenta_blocksize
self.chunksize = self.configuration.nexenta_chunksize
self.cluster, self.tenant, self.bucket = self.bucket_path.split('/')
self.bucket_url = ('clusters/' + self.cluster + '/tenants/' +
self.tenant + '/buckets/' + self.bucket)
self.hostname = socket.gethostname()
self.symlinks_dir = self.configuration.nexenta_nbd_symlinks_dir
self.reserved_percentage = self.configuration.reserved_percentage
LOG.debug('NexentaEdgeNBDDriver. Initialized successfully.')
@property
def backend_name(self):
backend_name = None
if self.configuration:
backend_name = self.configuration.safe_get('volume_backend_name')
if not backend_name:
backend_name = self.__class__.__name__
return backend_name
def do_setup(self, context):
if self.restapi_protocol == 'auto':
protocol, auto = 'http', True
else:
protocol, auto = self.restapi_protocol, False
self.restapi = jsonrpc.NexentaEdgeJSONProxy(
protocol, self.restapi_host, self.restapi_port, '',
self.restapi_user, self.restapi_password, auto=auto)
def check_for_setup_error(self):
try:
if not self.symlinks_dir:
msg = _("nexenta_nbd_symlinks_dir option is not specified")
raise exception.NexentaException(message=msg)
if not os.path.exists(self.symlinks_dir):
msg = _("NexentaEdge NBD symlinks directory doesn't exist")
raise exception.NexentaException(message=msg)
self.restapi.get(self.bucket_url + '/objects/')
except exception.VolumeBackendAPIException:
with excutils.save_and_reraise_exception():
LOG.exception(_LE('Error verifying container %(bkt)s'),
{'bkt': self.bucket_path})
def _get_nbd_devices(self, host):
try:
rsp = self.restapi.get('sysconfig/nbd/devices' +
self._get_remote_url(host))
except exception.VolumeBackendAPIException:
with excutils.save_and_reraise_exception():
LOG.exception(_LE('Error getting NBD list'))
return json.loads(rsp['value'])
def _get_nbd_number(self, volume):
host = volutils.extract_host(volume['host'], 'host')
nbds = self._get_nbd_devices(host)
for dev in nbds:
if dev['objectPath'] == self.bucket_path + '/' + volume['name']:
return dev['number']
return -1
def _get_host_info(self, host):
try:
res = self.restapi.get('system/stats')
servers = res['stats']['servers']
for sid in servers:
if host == sid or host == servers[sid]['hostname']:
return servers[sid]
except exception.VolumeBackendAPIException:
with excutils.save_and_reraise_exception():
LOG.exception(_LE('Error getting host info'))
raise exception.VolumeBackendAPIException(
data=_('No %s hostname in NEdge cluster') % host)
def _get_remote_url(self, host):
return '?remote=' + (
six.text_type(self._get_host_info(host)['ipv6addr']))
def _get_symlink_path(self, number):
return os.path.join(self.symlinks_dir, 'nbd' + six.text_type(number))
def local_path(self, volume):
number = self._get_nbd_number(volume)
if number == -1:
msg = _('No NBD device for volume %s') % volume['name']
raise exception.VolumeBackendAPIException(data=msg)
return self._get_symlink_path(number)
def create_volume(self, volume):
LOG.debug('Create volume')
host = volutils.extract_host(volume['host'], 'host')
try:
self.restapi.post('nbd' + self._get_remote_url(host), {
'objectPath': self.bucket_path + '/' + volume['name'],
'volSizeMB': int(volume['size']) * units.Ki,
'blockSize': self.blocksize,
'chunkSize': self.chunksize
})
number = self._get_nbd_number(volume)
cinder_utils.execute(
'ln', '--symbolic', '--force',
'/dev/nbd' + six.text_type(number),
self._get_symlink_path(number), run_as_root=True,
check_exit_code=True)
except exception.VolumeBackendAPIException:
with excutils.save_and_reraise_exception():
LOG.exception(_LE('Error creating volume'))
def delete_volume(self, volume):
LOG.debug('Delete volume')
number = self._get_nbd_number(volume)
if number == -1:
LOG.info(_LI('Volume %(volume)s does not exist at %(path)s '
'path') % {
'volume': volume['name'],
'path': self.bucket_path
})
return
host = volutils.extract_host(volume['host'], 'host')
try:
self.restapi.delete('nbd' + self._get_remote_url(host), {
'objectPath': self.bucket_path + '/' + volume['name'],
'number': number
})
except exception.VolumeBackendAPIException:
with excutils.save_and_reraise_exception():
LOG.exception(_LE('Error deleting volume'))
def extend_volume(self, volume, new_size):
LOG.debug('Extend volume')
host = volutils.extract_host(volume['host'], 'host')
try:
self.restapi.put('nbd/resize' + self._get_remote_url(host), {
'objectPath': self.bucket_path + '/' + volume['name'],
'newSizeMB': new_size * units.Ki
})
except exception.VolumeBackendAPIException:
with excutils.save_and_reraise_exception():
LOG.exception(_LE('Error extending volume'))
def create_snapshot(self, snapshot):
LOG.debug('Create snapshot')
try:
self.restapi.post('nbd/snapshot', {
'objectPath': self.bucket_path + '/' + snapshot['volume_name'],
'snapName': snapshot['name']
})
except exception.VolumeBackendAPIException:
with excutils.save_and_reraise_exception():
LOG.exception(_LE('Error creating snapshot'))
def delete_snapshot(self, snapshot):
LOG.debug('Delete snapshot')
# There is no way to figure out whether a snapshot exists in current
# version of the API. This REST function always reports OK even a
# snapshot doesn't exist.
try:
self.restapi.delete('nbd/snapshot', {
'objectPath': self.bucket_path + '/' + snapshot['volume_name'],
'snapName': snapshot['name']
})
except exception.VolumeBackendAPIException:
with excutils.save_and_reraise_exception():
LOG.exception(_LE('Error deleting snapshot'))
def create_volume_from_snapshot(self, volume, snapshot):
LOG.debug('Create volume from snapshot')
host = volutils.extract_host(volume['host'], 'host')
remotehost = self._get_remote_url(host)
try:
self.restapi.put('nbd/snapshot/clone' + remotehost, {
'objectPath': self.bucket_path + '/' + snapshot['volume_name'],
'snapName': snapshot['name'],
'clonePath': self.bucket_path + '/' + volume['name']
})
except exception.VolumeBackendAPIException:
with excutils.save_and_reraise_exception():
LOG.exception(_LE('Error cloning snapshot'))
if volume['size'] > snapshot['volume_size']:
self.extend_volume(volume, volume['size'])
def create_cloned_volume(self, volume, src_vref):
LOG.debug('Create cloned volume')
vol_url = (self.bucket_url + '/objects/' +
src_vref['name'] + '/clone')
clone_body = {
'tenant_name': self.tenant,
'bucket_name': self.bucket,
'object_name': volume['name']
}
host = volutils.extract_host(volume['host'], 'host')
size = volume['size'] if volume['size'] > src_vref['size'] else (
src_vref['size'])
try:
self.restapi.post(vol_url, clone_body)
self.restapi.post('nbd' + self._get_remote_url(host), {
'objectPath': self.bucket_path + '/' + volume['name'],
'volSizeMB': int(size) * units.Ki,
'blockSize': self.blocksize,
'chunkSize': self.chunksize
})
except exception.VolumeBackendAPIException:
with excutils.save_and_reraise_exception():
LOG.exception(_LE('Error creating cloned volume'))
def migrate_volume(self, ctxt, volume, host, thin=False, mirror_count=0):
raise NotImplemented
def get_volume_stats(self, refresh=False):
LOG.debug('Get volume stats')
try:
resp = self.restapi.get('system/stats')
location_info = '%(driver)s:%(host)s:%(bucket)s' % {
'driver': self.__class__.__name__,
'host': self.hostname,
'bucket': self.bucket_path
}
summary = resp['stats']['summary']
total = nexenta_utils.str2gib_size(summary['total_capacity'])
free = nexenta_utils.str2gib_size(summary['total_available'])
return {
'vendor_name': 'Nexenta',
'driver_version': self.VERSION,
'storage_protocol': 'NBD',
'reserved_percentage': self.reserved_percentage,
'total_capacity_gb': total,
'free_capacity_gb': free,
'QoS_support': False,
'volume_backend_name': self.backend_name,
'location_info': location_info,
'restapi_url': self.restapi.url
}
except exception.VolumeBackendAPIException:
with excutils.save_and_reraise_exception():
LOG.exception(_LE('Error creating snapshot'))
def copy_image_to_volume(self, context, volume, image_service, image_id):
LOG.debug('Copy image to volume')
image_utils.fetch_to_raw(context,
image_service,
image_id,
self.local_path(volume),
self.configuration.volume_dd_blocksize,
size=volume['size'])
def copy_volume_to_image(self, context, volume, image_service, image_meta):
LOG.debug('Copy volume to image')
image_utils.upload_volume(context,
image_service,
image_meta,
self.local_path(volume))
def ensure_export(self, context, volume):
pass
def create_export(self, context, volume, connector, vg=None):
pass
def remove_export(self, context, volume):
pass
def validate_connector(self, connector):
LOG.debug('Validate connector')
try:
res = self.restapi.get('system/stats')
servers = res['stats']['servers']
for sid in servers:
if (connector['host'] == sid or
connector['host'] == servers[sid]['hostname']):
return
except exception.VolumeBackendAPIException:
with excutils.save_and_reraise_exception():
LOG.exception(_LE('Error retrieving cluster stats'))
raise exception.VolumeBackendAPIException(
data=_('No %s hostname in NEdge cluster') % connector['host'])
def initialize_connection(self, volume, connector, initiator_data=None):
LOG.debug('Initialize connection')
return {
'driver_volume_type': 'local',
'data': {'device_path': self.local_path(volume)},
}

View File

@ -17,6 +17,10 @@ from oslo_config import cfg
NEXENTA_EDGE_OPTS = [ NEXENTA_EDGE_OPTS = [
cfg.StrOpt('nexenta_nbd_symlinks_dir',
default='/dev/disk/by-path',
help='NexentaEdge logical path of directory to store symbolic '
'links to NBDs'),
cfg.StrOpt('nexenta_rest_address', cfg.StrOpt('nexenta_rest_address',
default='', default='',
help='IP address of NexentaEdge management REST API endpoint'), help='IP address of NexentaEdge management REST API endpoint'),

View File

@ -0,0 +1,2 @@
features:
- Added NBD driver for NexentaEdge.