]> review.fuel-infra Code Review - openstack-build/cinder-build.git/commitdiff
Nexenta Edge iSCSI backend driver
authormikhail <mikhail.khodos@nexenta.com>
Tue, 24 Feb 2015 16:00:43 +0000 (08:00 -0800)
committerSean McGinnis <sean_mcginnis@dell.com>
Wed, 25 Nov 2015 23:37:37 +0000 (17:37 -0600)
Nexenta Edge project supports iSCSI block level storage.
This patch implements a driver for Nexenta Edge iSCSI backend.

DocImpact
Implements: blueprint nexentaedge-iscsi-volume-driver
Change-Id: I82c215ba85e9d49723e792d88d86553b3a75d3ac

cinder/opts.py
cinder/tests/unit/test_nexenta_edge.py [new file with mode: 0644]
cinder/volume/drivers/nexenta/__init__.py [new file with mode: 0644]
cinder/volume/drivers/nexenta/nexentaedge/__init__.py [new file with mode: 0644]
cinder/volume/drivers/nexenta/nexentaedge/iscsi.py [new file with mode: 0644]
cinder/volume/drivers/nexenta/nexentaedge/jsonrpc.py [new file with mode: 0644]
tests-py3.txt

index 8d65f80314b1a74e8f9adb91b8127d47e52ecffc..712a80347b5ead36f0b6caa8b03175346fe09a14 100644 (file)
@@ -119,6 +119,8 @@ from cinder.volume.drivers.lenovo import lenovo_common as \
 from cinder.volume.drivers import lvm as cinder_volume_drivers_lvm
 from cinder.volume.drivers.netapp import options as \
     cinder_volume_drivers_netapp_options
+from cinder.volume.drivers.nexenta.nexentaedge import iscsi as \
+    cinder_volume_drivers_nexenta_nexentaedge_iscsi
 from cinder.volume.drivers import nfs as cinder_volume_drivers_nfs
 from cinder.volume.drivers import nimble as cinder_volume_drivers_nimble
 from cinder.volume.drivers.prophetstor import options as \
@@ -287,6 +289,8 @@ def list_opts():
                 cinder_volume_drivers_prophetstor_options.DPL_OPTS,
                 cinder_volume_drivers_hitachi_hbsdiscsi.volume_opts,
                 cinder_volume_manager.volume_manager_opts,
+                cinder_volume_drivers_nexenta_nexentaedge_iscsi.
+                nexenta_edge_opts,
                 cinder_volume_drivers_ibm_flashsystemiscsi.
                 flashsystem_iscsi_opts,
                 cinder_volume_drivers_ibm_flashsystemcommon.flashsystem_opts,
diff --git a/cinder/tests/unit/test_nexenta_edge.py b/cinder/tests/unit/test_nexenta_edge.py
new file mode 100644 (file)
index 0000000..a6426cd
--- /dev/null
@@ -0,0 +1,169 @@
+#
+# Copyright 2015 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 mock
+
+from cinder import context
+from cinder import test
+from cinder.volume import configuration as conf
+from cinder.volume.drivers.nexenta.nexentaedge import iscsi
+
+NEDGE_URL = 'service/isc/iscsi'
+NEDGE_BUCKET = 'c/t/bk'
+NEDGE_SERVICE = 'isc'
+NEDGE_BLOCKSIZE = 4096
+NEDGE_CHUNKSIZE = 16384
+
+MOCK_VOL = {
+    'id': 'vol1',
+    'name': 'vol1',
+    'size': 1
+}
+MOCK_VOL2 = {
+    'id': 'vol2',
+    'name': 'vol2',
+    'size': 1
+}
+MOCK_SNAP = {
+    'id': 'snap1',
+    'name': 'snap1',
+    'volume_name': 'vol1'
+}
+NEW_VOL_SIZE = 2
+ISCSI_TARGET_NAME = 'iscsi_target_name'
+ISCSI_TARGET_STATUS = 'Target 1: ' + ISCSI_TARGET_NAME
+
+
+class TestNexentaEdgeISCSIDriver(test.TestCase):
+
+    def setUp(self):
+        super(TestNexentaEdgeISCSIDriver, self).setUp()
+        self.cfg = mock.Mock(spec=conf.Configuration)
+        self.cfg.nexenta_client_address = '0.0.0.0'
+        self.cfg.nexenta_rest_address = '0.0.0.0'
+        self.cfg.nexenta_rest_port = 8080
+        self.cfg.nexenta_rest_protocol = 'http'
+        self.cfg.nexenta_iscsi_target_portal_port = 3260
+        self.cfg.nexenta_rest_user = 'admin'
+        self.cfg.nexenta_rest_password = 'admin'
+        self.cfg.nexenta_lun_container = NEDGE_BUCKET
+        self.cfg.nexenta_iscsi_service = NEDGE_SERVICE
+        self.cfg.nexenta_blocksize = NEDGE_BLOCKSIZE
+        self.cfg.nexenta_chunksize = NEDGE_CHUNKSIZE
+
+        mock_exec = mock.Mock()
+        mock_exec.return_value = ('', '')
+        self.driver = iscsi.NexentaEdgeISCSIDriver(execute=mock_exec,
+                                                   configuration=self.cfg)
+        self.api_patcher = mock.patch('cinder.volume.drivers.nexenta.'
+                                      'nexentaedge.jsonrpc.'
+                                      'NexentaEdgeJSONProxy.__call__')
+        self.mock_api = self.api_patcher.start()
+
+        self.mock_api.return_value = {
+            'data': {'value': ISCSI_TARGET_STATUS}
+        }
+        self.driver.do_setup(context.get_admin_context())
+
+        self.addCleanup(self.api_patcher.stop)
+
+    def test_check_do_setup(self):
+        self.assertEqual(ISCSI_TARGET_NAME, self.driver.target_name)
+
+    def test_create_volume(self):
+        self.driver.create_volume(MOCK_VOL)
+        self.mock_api.assert_called_with(NEDGE_URL, {
+            'objectPath': NEDGE_BUCKET + '/' + MOCK_VOL['id'],
+            'volSizeMB': MOCK_VOL['size'] * 1024,
+            'blockSize': NEDGE_BLOCKSIZE,
+            'chunkSize': NEDGE_CHUNKSIZE
+        })
+
+    def test_create_volume_fail(self):
+        self.mock_api.side_effect = RuntimeError
+        self.assertRaises(RuntimeError, self.driver.create_volume, MOCK_VOL)
+
+    def test_delete_volume(self):
+        self.driver.delete_volume(MOCK_VOL)
+        self.mock_api.assert_called_with(NEDGE_URL, {
+            'objectPath': NEDGE_BUCKET + '/' + MOCK_VOL['id']
+        })
+
+    def test_delete_volume_fail(self):
+        self.mock_api.side_effect = RuntimeError
+        self.assertRaises(RuntimeError, self.driver.delete_volume, MOCK_VOL)
+
+    def test_extend_volume(self):
+        self.driver.extend_volume(MOCK_VOL, NEW_VOL_SIZE)
+        self.mock_api.assert_called_with(NEDGE_URL + '/resize', {
+            'objectPath': NEDGE_BUCKET + '/' + MOCK_VOL['id'],
+            'newSizeMB': NEW_VOL_SIZE * 1024
+        })
+
+    def test_extend_volume_fail(self):
+        self.mock_api.side_effect = RuntimeError
+        self.assertRaises(RuntimeError, self.driver.extend_volume,
+                          MOCK_VOL, NEW_VOL_SIZE)
+
+    def test_create_snapshot(self):
+        self.driver.create_snapshot(MOCK_SNAP)
+        self.mock_api.assert_called_with(NEDGE_URL + '/snapshot', {
+            'objectPath': NEDGE_BUCKET + '/' + MOCK_VOL['id'],
+            'snapName': MOCK_SNAP['id']
+        })
+
+    def test_create_snapshot_fail(self):
+        self.mock_api.side_effect = RuntimeError
+        self.assertRaises(RuntimeError, self.driver.create_snapshot, MOCK_SNAP)
+
+    def test_delete_snapshot(self):
+        self.driver.delete_snapshot(MOCK_SNAP)
+        self.mock_api.assert_called_with(NEDGE_URL + '/snapshot', {
+            'objectPath': NEDGE_BUCKET + '/' + MOCK_VOL['id'],
+            'snapName': MOCK_SNAP['id']
+        })
+
+    def test_delete_snapshot_fail(self):
+        self.mock_api.side_effect = RuntimeError
+        self.assertRaises(RuntimeError, self.driver.delete_snapshot, MOCK_SNAP)
+
+    def test_create_volume_from_snapshot(self):
+        self.driver.create_volume_from_snapshot(MOCK_VOL2, MOCK_SNAP)
+        self.mock_api.assert_called_with(NEDGE_URL + '/snapshot/clone', {
+            'objectPath': NEDGE_BUCKET + '/' + MOCK_SNAP['volume_name'],
+            'clonePath': NEDGE_BUCKET + '/' + MOCK_VOL2['id'],
+            'snapName': MOCK_SNAP['id']
+        })
+
+    def test_create_volume_from_snapshot_fail(self):
+        self.mock_api.side_effect = RuntimeError
+        self.assertRaises(RuntimeError,
+                          self.driver.create_volume_from_snapshot,
+                          MOCK_VOL2, MOCK_SNAP)
+
+    def test_create_cloned_volume(self):
+        self.driver.create_cloned_volume(MOCK_VOL2, MOCK_VOL)
+        self.mock_api.assert_called_with(NEDGE_URL, {
+            'objectPath': NEDGE_BUCKET + '/' + MOCK_VOL2['id'],
+            'volSizeMB': MOCK_VOL2['size'] * 1024,
+            'blockSize': NEDGE_BLOCKSIZE,
+            'chunkSize': NEDGE_CHUNKSIZE
+        })
+
+    def test_create_cloned_volume_fail(self):
+        self.mock_api.side_effect = RuntimeError
+        self.assertRaises(RuntimeError, self.driver.create_cloned_volume,
+                          MOCK_VOL2, MOCK_VOL)
diff --git a/cinder/volume/drivers/nexenta/__init__.py b/cinder/volume/drivers/nexenta/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/cinder/volume/drivers/nexenta/nexentaedge/__init__.py b/cinder/volume/drivers/nexenta/nexentaedge/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/cinder/volume/drivers/nexenta/nexentaedge/iscsi.py b/cinder/volume/drivers/nexenta/nexentaedge/iscsi.py
new file mode 100644 (file)
index 0000000..6bf4c67
--- /dev/null
@@ -0,0 +1,303 @@
+# Copyright 2015 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
+
+from oslo_config import cfg
+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
+from cinder.volume import driver
+from cinder.volume.drivers.nexenta.nexentaedge import jsonrpc
+
+
+nexenta_edge_opts = [
+    cfg.StrOpt('nexenta_rest_address',
+               default='',
+               help='IP address of NexentaEdge management REST API endpoint'),
+    cfg.IntOpt('nexenta_rest_port',
+               default=8080,
+               help='HTTP port to connect to NexentaEdge REST API endpoint'),
+    cfg.StrOpt('nexenta_rest_protocol',
+               default='auto',
+               help='Use http or https for REST connection (default auto)'),
+    cfg.IntOpt('nexenta_iscsi_target_portal_port',
+               default=3260,
+               help='NexentaEdge target portal port'),
+    cfg.StrOpt('nexenta_rest_user',
+               default='admin',
+               help='User name to connect to NexentaEdge'),
+    cfg.StrOpt('nexenta_rest_password',
+               default='nexenta',
+               help='Password to connect to NexentaEdge',
+               secret=True),
+    cfg.StrOpt('nexenta_lun_container',
+               default='',
+               help='NexentaEdge logical path of bucket for LUNs'),
+    cfg.StrOpt('nexenta_iscsi_service',
+               default='',
+               help='NexentaEdge iSCSI service name'),
+    cfg.StrOpt('nexenta_client_address',
+               default='',
+               help='NexentaEdge iSCSI Gateway client '
+               'address for non-VIP service'),
+    cfg.StrOpt('nexenta_blocksize',
+               default=4096,
+               help='NexentaEdge iSCSI LUN block size'),
+    cfg.StrOpt('nexenta_chunksize',
+               default=16384,
+               help='NexentaEdge iSCSI LUN object chunk size')
+]
+
+CONF = cfg.CONF
+CONF.register_opts(nexenta_edge_opts)
+
+LOG = logging.getLogger(__name__)
+
+
+class NexentaEdgeISCSIDriver(driver.ISCSIDriver):
+    """Executes volume driver commands on NexentaEdge cluster.
+
+    Version history:
+        1.0.0 - Initial driver version.
+    """
+
+    VERSION = '1.0.0'
+
+    def __init__(self, *args, **kwargs):
+        super(NexentaEdgeISCSIDriver, self).__init__(*args, **kwargs)
+        if self.configuration:
+            self.configuration.append_config_values(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.iscsi_service = self.configuration.nexenta_iscsi_service
+        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.iscsi_target_port = (self.configuration.
+                                  nexenta_iscsi_target_portal_port)
+        self.target_vip = None
+
+    @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
+
+        try:
+            self.restapi = jsonrpc.NexentaEdgeJSONProxy(
+                protocol, self.restapi_host, self.restapi_port, '/',
+                self.restapi_user, self.restapi_password, auto=auto)
+
+            rsp = self.restapi.get('service/'
+                                   + self.iscsi_service + '/iscsi/status')
+            data_keys = rsp['data'][list(rsp['data'].keys())[0]]
+            self.target_name = data_keys.split('\n', 1)[0].split(' ')[2]
+
+            rsp = self.restapi.get('service/' + self.iscsi_service)
+            if 'X-VIPS' in rsp['data']:
+                vips = json.loads(rsp['data']['X-VIPS'])
+                if len(vips[0]) == 1:
+                    self.target_vip = vips[0][0]['ip'].split('/', 1)[0]
+                else:
+                    self.target_vip = vips[0][1]['ip'].split('/', 1)[0]
+            else:
+                self.target_vip = self.configuration.safe_get(
+                    'nexenta_client_address')
+                if not self.target_vip:
+                    LOG.error(_LE('No VIP configured for service %s'),
+                              self.iscsi_service)
+                    raise exception.VolumeBackendAPIException(
+                        _('No service VIP configured and '
+                          'no nexenta_client_address'))
+        except exception.VolumeBackendAPIException:
+            with excutils.save_and_reraise_exception():
+                LOG.exception(_LE('Error verifying iSCSI service %(serv)s on '
+                              'host %(hst)s'), {'serv': self.iscsi_service,
+                              'hst': self.restapi_host})
+
+    def check_for_setup_error(self):
+        try:
+            self.restapi.get(self.bucket_url + '/objects/')
+        except exception.VolumeBackendAPIException:
+            with excutils.save_and_reraise_exception():
+                LOG.exception(_LE('Error verifying LUN container %(bkt)s'),
+                              {'bkt': self.bucket_path})
+
+    def _get_lun_number(self, volname):
+        try:
+            rsp = self.restapi.put(
+                'service/' + self.iscsi_service + '/iscsi/number',
+                {
+                    'objectPath': self.bucket_path + '/' + volname
+                })
+        except exception.VolumeBackendAPIException:
+            with excutils.save_and_reraise_exception():
+                LOG.exception(_LE('Error retrieving LUN %(vol)s number'),
+                              {'vol': volname})
+
+        return rsp['data']
+
+    def _get_target_address(self, volname):
+        return self.target_vip
+
+    def _get_provider_location(self, volume):
+        return '%(host)s:%(port)s,1 %(name)s %(number)s' % {
+            'host': self._get_target_address(volume['name']),
+            'port': self.iscsi_target_port,
+            'name': self.target_name,
+            'number': self._get_lun_number(volume['name'])
+        }
+
+    def create_volume(self, volume):
+        try:
+            self.restapi.post('service/' + self.iscsi_service + '/iscsi', {
+                'objectPath': self.bucket_path + '/' + volume['name'],
+                'volSizeMB': int(volume['size']) * units.Ki,
+                'blockSize': self.blocksize,
+                'chunkSize': self.chunksize
+            })
+        except exception.VolumeBackendAPIException:
+            with excutils.save_and_reraise_exception():
+                LOG.exception(_LE('Error creating volume'))
+
+    def delete_volume(self, volume):
+        try:
+            self.restapi.delete('service/' + self.iscsi_service +
+                                '/iscsi', {'objectPath': self.bucket_path +
+                                           '/' + volume['name']})
+        except exception.VolumeBackendAPIException:
+            with excutils.save_and_reraise_exception():
+                LOG.exception(_LE('Error deleting volume'))
+
+    def extend_volume(self, volume, new_size):
+        try:
+            self.restapi.put('service/' + self.iscsi_service + '/iscsi/resize',
+                             {'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_volume_from_snapshot(self, volume, snapshot):
+        try:
+            self.restapi.put(
+                'service/' + self.iscsi_service + '/iscsi/snapshot/clone',
+                {
+                    'objectPath': self.bucket_path + '/' +
+                    snapshot['volume_name'],
+                    'clonePath': self.bucket_path + '/' + volume['name'],
+                    'snapName': snapshot['name']
+                })
+        except exception.VolumeBackendAPIException:
+            with excutils.save_and_reraise_exception():
+                LOG.exception(_LE('Error cloning volume'))
+
+    def create_snapshot(self, snapshot):
+        try:
+            self.restapi.post(
+                'service/' + self.iscsi_service + '/iscsi/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):
+        try:
+            self.restapi.delete(
+                'service/' + self.iscsi_service + '/iscsi/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_cloned_volume(self, volume, src_vref):
+        vol_url = (self.bucket_url + '/objects/' +
+                   src_vref['name'] + '/clone')
+        clone_body = {
+            'tenant_name': self.tenant,
+            'bucket_name': self.bucket,
+            'object_name': volume['name']
+        }
+        try:
+            self.restapi.post(vol_url, clone_body)
+            self.restapi.post('service/' + self.iscsi_service + '/iscsi', {
+                'objectPath': self.bucket_path + '/' + volume['name'],
+                'volSizeMB': int(src_vref['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 create_export(self, context, volume, connector=None):
+        return {'provider_location': self._get_provider_location(volume)}
+
+    def ensure_export(self, context, volume):
+        pass
+
+    def remove_export(self, context, volume):
+        pass
+
+    def local_path(self, volume):
+        raise NotImplementedError
+
+    def get_volume_stats(self, refresh=False):
+        location_info = '%(driver)s:%(host)s:%(bucket)s' % {
+            'driver': self.__class__.__name__,
+            'host': self._get_target_address(None),
+            'bucket': self.bucket_path
+        }
+        return {
+            'vendor_name': 'Nexenta',
+            'driver_version': self.VERSION,
+            'storage_protocol': 'iSCSI',
+            'reserved_percentage': 0,
+            'total_capacity_gb': 'unknown',
+            'free_capacity_gb': 'unknown',
+            'QoS_support': False,
+            'volume_backend_name': self.backend_name,
+            'location_info': location_info,
+            'iscsi_target_portal_port': self.iscsi_target_port,
+            'restapi_url': self.restapi.url
+        }
diff --git a/cinder/volume/drivers/nexenta/nexentaedge/jsonrpc.py b/cinder/volume/drivers/nexenta/nexentaedge/jsonrpc.py
new file mode 100644 (file)
index 0000000..10d2e83
--- /dev/null
@@ -0,0 +1,100 @@
+# Copyright 2015 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 requests
+import socket
+
+from oslo_log import log as logging
+
+from cinder import exception
+from cinder.i18n import _
+from cinder.utils import retry
+
+LOG = logging.getLogger(__name__)
+socket.setdefaulttimeout(100)
+
+
+class NexentaEdgeJSONProxy(object):
+
+    retry_exc_tuple = (
+        requests.exceptions.ConnectionError,
+        requests.exceptions.ConnectTimeout
+    )
+
+    def __init__(self, protocol, host, port, path, user, password, auto=False,
+                 method=None):
+        self.protocol = protocol.lower()
+        self.host = host
+        self.port = port
+        self.path = path
+        self.user = user
+        self.password = password
+        self.auto = auto
+        self.method = method
+
+    @property
+    def url(self):
+        return '%s://%s:%s%s' % (self.protocol,
+                                 self.host, self.port, self.path)
+
+    def __getattr__(self, name):
+        if not self.method:
+            method = name
+        else:
+            raise exception.VolumeDriverException(
+                _("Wrong resource call syntax"))
+        return NexentaEdgeJSONProxy(
+            self.protocol, self.host, self.port, self.path,
+            self.user, self.password, self.auto, method)
+
+    def __hash__(self):
+        return self.url.__hash___()
+
+    def __repr__(self):
+        return 'HTTP JSON proxy: %s' % self.url
+
+    @retry(retry_exc_tuple, interval=1, retries=6)
+    def __call__(self, *args):
+        self.path += args[0]
+        data = None
+        if len(args) > 1:
+            data = json.dumps(args[1])
+
+        auth = ('%s:%s' % (self.user, self.password)).encode('base64')[:-1]
+        headers = {
+            'Content-Type': 'application/json',
+            'Authorization': 'Basic %s' % auth
+        }
+
+        LOG.debug('Sending JSON data: %s', self.url)
+
+        if self.method == 'get':
+            req = requests.get(self.url, headers=headers)
+        if self.method == 'post':
+            req = requests.post(self.url, data=data, headers=headers)
+        if self.method == 'put':
+            req = requests.put(self.url, data=data, headers=headers)
+        if self.method == 'delete':
+            req = requests.delete(self.url, data=data, headers=headers)
+
+        rsp = req.json()
+        req.close()
+
+        LOG.debug('Got response: %s', rsp)
+        if rsp.get('response') is None:
+            raise exception.VolumeBackendAPIException(
+                _('Error response: %s') % rsp)
+        return rsp.get('response')
index d071dda3fc4583e10d504f243d3116247bc5d6c7..febfce0d2d32c537dcfbde05a71c5150cadf6696 100644 (file)
@@ -92,6 +92,7 @@ cinder.tests.unit.test_misc
 cinder.tests.unit.test_netapp
 cinder.tests.unit.test_netapp_nfs
 cinder.tests.unit.test_netapp_ssc
+cinder.tests.unit.test_nexenta_edge
 cinder.tests.unit.test_nfs
 cinder.tests.unit.test_nimble
 cinder.tests.unit.test_prophetstor_dpl