From ca9e590f8204032b55609d6304be95a5c35cd23d Mon Sep 17 00:00:00 2001 From: Aleksey Ruban Date: Mon, 23 May 2016 11:00:33 -0600 Subject: [PATCH] Nexenta: Add NBD driver for NexentaEdge. DocImpact Implements: blueprint nexentaedge-ndb-driver Change-Id: Iead87f00d75bfa93a06c7d2c1c6ab708bc5d804b --- cinder/tests/unit/test_nexenta_edge_nbd.py | 518 ++++++++++++++++++ .../drivers/nexenta/nexentaedge/jsonrpc.py | 18 +- .../volume/drivers/nexenta/nexentaedge/nbd.py | 346 ++++++++++++ cinder/volume/drivers/nexenta/options.py | 4 + .../nexentaedge-nbd-eb48268723141f12.yaml | 2 + 5 files changed, 882 insertions(+), 6 deletions(-) create mode 100644 cinder/tests/unit/test_nexenta_edge_nbd.py create mode 100644 cinder/volume/drivers/nexenta/nexentaedge/nbd.py create mode 100644 releasenotes/notes/nexentaedge-nbd-eb48268723141f12.yaml diff --git a/cinder/tests/unit/test_nexenta_edge_nbd.py b/cinder/tests/unit/test_nexenta_edge_nbd.py new file mode 100644 index 00000000000..33a1106e7d9 --- /dev/null +++ b/cinder/tests/unit/test_nexenta_edge_nbd.py @@ -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)) diff --git a/cinder/volume/drivers/nexenta/nexentaedge/jsonrpc.py b/cinder/volume/drivers/nexenta/nexentaedge/jsonrpc.py index fce277a289d..9baf29bc0dd 100644 --- a/cinder/volume/drivers/nexenta/nexentaedge/jsonrpc.py +++ b/cinder/volume/drivers/nexenta/nexentaedge/jsonrpc.py @@ -13,6 +13,7 @@ # License for the specific language governing permissions and limitations # under the License. +import base64 import json import requests import socket @@ -73,22 +74,27 @@ class NexentaEdgeJSONProxy(object): if len(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 = { 'Content-Type': 'application/json', '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': req = requests.get(self.url, headers=headers) - if self.method == 'post': + elif self.method == 'post': 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) - if self.method == 'delete': + elif self.method == 'delete': req = requests.delete(self.url, data=data, headers=headers) + else: + raise exception.VolumeDriverException( + message=_('Unsupported method: %s') % self.method) rsp = req.json() req.close() @@ -96,5 +102,5 @@ class NexentaEdgeJSONProxy(object): LOG.debug('Got response: %s', rsp) if rsp.get('response') is None: raise exception.VolumeBackendAPIException( - _('Error response: %s') % rsp) + data=_('Error response: %s') % rsp) return rsp.get('response') diff --git a/cinder/volume/drivers/nexenta/nexentaedge/nbd.py b/cinder/volume/drivers/nexenta/nexentaedge/nbd.py new file mode 100644 index 00000000000..518b4ea57a6 --- /dev/null +++ b/cinder/volume/drivers/nexenta/nexentaedge/nbd.py @@ -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)}, + } diff --git a/cinder/volume/drivers/nexenta/options.py b/cinder/volume/drivers/nexenta/options.py index 463b9c525ee..0518b25d881 100644 --- a/cinder/volume/drivers/nexenta/options.py +++ b/cinder/volume/drivers/nexenta/options.py @@ -17,6 +17,10 @@ from oslo_config import cfg 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', default='', help='IP address of NexentaEdge management REST API endpoint'), diff --git a/releasenotes/notes/nexentaedge-nbd-eb48268723141f12.yaml b/releasenotes/notes/nexentaedge-nbd-eb48268723141f12.yaml new file mode 100644 index 00000000000..3f8f2fb2b29 --- /dev/null +++ b/releasenotes/notes/nexentaedge-nbd-eb48268723141f12.yaml @@ -0,0 +1,2 @@ +features: + - Added NBD driver for NexentaEdge.