From 961d5b0cd92b23e4e877c748391cb3cdaef5a285 Mon Sep 17 00:00:00 2001 From: Alexey Khodos Date: Sat, 23 Jan 2016 00:55:45 +0300 Subject: [PATCH] NexentaStor5 iSCSI driver unit tests Additional unit tests to cover jsonrpc and iscsi exceptions. Updating the delete behavior to not throw an exception if the LU was already deleted on the appliance. Also fixing an issue with the total and free space in get_stats. Change-Id: I272a4085726e1162ace0b1a5cad9989e294bdf93 --- cinder/tests/unit/test_nexenta5_iscsi.py | 179 ++++++++++++++++++- cinder/volume/drivers/nexenta/ns5/iscsi.py | 16 +- cinder/volume/drivers/nexenta/ns5/jsonrpc.py | 19 +- 3 files changed, 190 insertions(+), 24 deletions(-) diff --git a/cinder/tests/unit/test_nexenta5_iscsi.py b/cinder/tests/unit/test_nexenta5_iscsi.py index 91074b8b6..e8e470836 100644 --- a/cinder/tests/unit/test_nexenta5_iscsi.py +++ b/cinder/tests/unit/test_nexenta5_iscsi.py @@ -18,7 +18,9 @@ Unit tests for OpenStack Cinder volume driver import mock from mock import patch +from oslo_serialization import jsonutils from oslo_utils import units +import requests from cinder import context from cinder import db @@ -92,12 +94,26 @@ class TestNexentaISCSIDriver(test.TestCase): } return db.volume_create(self.ctxt, vol)['id'] - def check_for_setup_error(self): + def test_do_setup(self): + self.nef_mock.post.side_effect = exception.NexentaException( + 'Could not create volume group') + self.assertRaises( + exception.NexentaException, + self.drv.do_setup, self.ctxt) + + self.nef_mock.post.side_effect = exception.NexentaException( + '{"code": "EEXIST"}') + self.assertIsNone(self.drv.do_setup(self.ctxt)) + + def test_check_for_setup_error(self): self.nef_mock.get.return_value = { - 'services': {'data': {'iscsit': {'state': 'offline'}}}} + 'data': [{'name': 'iscsit', 'state': 'offline'}]} self.assertRaises( exception.NexentaException, self.drv.check_for_setup_error) + self.nef_mock.get.side_effect = exception.NexentaException() + self.assertRaises(LookupError, self.drv.check_for_setup_error) + def test_create_volume(self): self.drv.create_volume(self.TEST_VOLUME_REF) url = 'storage/pools/pool/volumeGroups/dsg/volumes' @@ -108,19 +124,45 @@ class TestNexentaISCSIDriver(test.TestCase): 'sparseVolume': self.cfg.nexenta_sparse}) def test_delete_volume(self): - self.drv.delete_volume(self.TEST_VOLUME_REF) + self.nef_mock.delete.side_effect = exception.NexentaException() + self.assertIsNone(self.drv.delete_volume(self.TEST_VOLUME_REF)) url = 'storage/pools/pool/volumeGroups' data = {'name': 'dsg', 'volumeBlockSize': 32768} self.nef_mock.post.assert_called_with(url, data) - def test_create_cloned_volume(self): + def test_extend_volume(self): + self.nef_mock.put.side_effect = exception.NexentaException() + self.assertRaises( + exception.NexentaException, + self.drv.extend_volume, self.TEST_VOLUME_REF, 2) + + def test_delete_snapshot(self): + self._create_volume_db_entry() + self.nef_mock.delete.side_effect = exception.NexentaException('EBUSY') + self.drv.delete_snapshot(self.TEST_SNAPSHOT_REF) + url = ('storage/pools/pool/volumeGroups/dsg/' + 'volumes/volume-1/snapshots/snapshot1') + self.nef_mock.delete.assert_called_with(url) + + self.nef_mock.delete.side_effect = exception.NexentaException('Error') + self.drv.delete_snapshot(self.TEST_SNAPSHOT_REF) + self.nef_mock.delete.assert_called_with(url) + + @patch('cinder.volume.drivers.nexenta.ns5.iscsi.' + 'NexentaISCSIDriver.create_snapshot') + @patch('cinder.volume.drivers.nexenta.ns5.iscsi.' + 'NexentaISCSIDriver.delete_snapshot') + @patch('cinder.volume.drivers.nexenta.ns5.iscsi.' + 'NexentaISCSIDriver.create_volume_from_snapshot') + def test_create_cloned_volume(self, crt_vol, dlt_snap, crt_snap): self._create_volume_db_entry() vol = self.TEST_VOLUME_REF2 src_vref = self.TEST_VOLUME_REF - - self.drv.create_cloned_volume(vol, src_vref) - url = 'storage/pools/pool/volumeGroups/dsg/volumes/volume2/promote' - self.nef_mock.post.assert_called_with(url) + crt_vol.side_effect = exception.NexentaException() + dlt_snap.side_effect = exception.NexentaException() + self.assertRaises( + exception.NexentaException, + self.drv.create_cloned_volume, vol, src_vref) def test_create_snapshot(self): self._create_volume_db_entry() @@ -149,13 +191,18 @@ class TestNexentaISCSIDriver(test.TestCase): @patch('cinder.volume.drivers.nexenta.ns5.iscsi.' 'NexentaISCSIDriver._get_target_by_alias') def test_create_target(self, target): + self.nef_mock.get.return_value = {} target.return_value = {'name': 'iqn-0'} self.assertEqual('iqn-0', self.drv._create_target(0)) + target.return_value = None + self.assertRaises(TypeError, self.drv._create_target, 0) + @patch('cinder.volume.drivers.nexenta.ns5.iscsi.' 'NexentaISCSIDriver._create_target') def test_get_target_name(self, target_name): self._create_volume_db_entry() + self.drv.targets = {} target_name.return_value = 'iqn-0' self.drv.targets['iqn-0'] = [] self.assertEqual( @@ -168,6 +215,16 @@ class TestNexentaISCSIDriver(test.TestCase): 'iqn-0', self.drv._get_target_name(self.TEST_VOLUME_REF)) self.assertEqual('1.1.1.1-0', self.drv.targetgroups['iqn-0']) + @patch('cinder.volume.drivers.nexenta.ns5.iscsi.' + 'NexentaISCSIDriver._create_target') + def test_get_targetgroup_name(self, target_name): + self.TEST_VOLUME_REF['provider_location'] = '1.1.1.1:8080,1 iqn-0 0' + self._create_volume_db_entry() + target_name = 'iqn-0' + self.drv.targetgroups[target_name] = '1.1.1.1-0' + self.assertEqual( + '1.1.1.1-0', self.drv._get_targetgroup_name(self.TEST_VOLUME_REF)) + @patch('cinder.volume.drivers.nexenta.ns5.iscsi.' 'NexentaISCSIDriver._get_targetgroup_name') def test_get_lun_id(self, targetgroup): @@ -175,6 +232,10 @@ class TestNexentaISCSIDriver(test.TestCase): self.nef_mock.get.return_value = {'data': [{'guid': '0'}]} self.assertEqual('0', self.drv._get_lun_id(self.TEST_VOLUME_REF)) + self.nef_mock.get.return_value = {} + self.assertRaises( + LookupError, self.drv._get_lun_id, self.TEST_VOLUME_REF) + @patch('cinder.volume.drivers.nexenta.ns5.iscsi.' 'NexentaISCSIDriver._get_lun_id') def test_lu_exists(self, lun_id): @@ -193,6 +254,13 @@ class TestNexentaISCSIDriver(test.TestCase): self.nef_mock.get.return_value = {'data': [{'lunNumber': 0}]} self.assertEqual(0, self.drv._get_lun(self.TEST_VOLUME_REF)) + self.nef_mock.get.return_value = {} + self.assertRaises( + LookupError, self.drv._get_lun, self.TEST_VOLUME_REF) + + lun_id.side_effect = LookupError() + self.assertIsNone(self.drv._get_lun(self.TEST_VOLUME_REF)) + @patch('cinder.volume.drivers.nexenta.ns5.iscsi.' 'NexentaISCSIDriver._get_target_name') @patch('cinder.volume.drivers.nexenta.ns5.iscsi.' @@ -209,3 +277,98 @@ class TestNexentaISCSIDriver(test.TestCase): self.assertEqual( {'provider_location': '1.1.1.1:8080,1 iqn-0 0'}, self.drv._do_export({}, self.TEST_VOLUME_REF)) + + @patch('cinder.volume.drivers.nexenta.ns5.iscsi.' + 'NexentaISCSIDriver._get_targetgroup_name') + @patch('cinder.volume.drivers.nexenta.ns5.iscsi.' + 'NexentaISCSIDriver._get_lun_id') + def test_remove_export(self, lun_id, tg_name): + lun_id.return_value = '0' + tg_name.return_value = '1.1.1.1-0' + self.nef_mock.delete.side_effect = exception.NexentaException( + 'No such logical unit in target group') + self.assertIsNone( + self.drv.remove_export(self.ctxt, self.TEST_VOLUME_REF)) + + self.nef_mock.delete.side_effect = exception.NexentaException( + 'Error') + self.assertRaises( + exception.NexentaException, + self.drv.remove_export, self.ctxt, self.TEST_VOLUME_REF) + + lun_id.side_effect = LookupError() + self.assertIsNone( + self.drv.remove_export(self.ctxt, self.TEST_VOLUME_REF)) + + def test_update_volume_stats(self): + self.nef_mock.get.return_value = { + 'bytesAvailable': 10 * units.Gi, + 'bytesUsed': 2 * units.Gi + } + location_info = '%(driver)s:%(host)s:%(pool)s/%(group)s' % { + 'driver': self.drv.__class__.__name__, + 'host': self.cfg.nexenta_host, + 'pool': self.cfg.nexenta_volume, + 'group': self.cfg.nexenta_volume_group, + } + stats = { + 'vendor_name': 'Nexenta', + 'dedup': self.cfg.nexenta_dataset_dedup, + 'compression': self.cfg.nexenta_dataset_compression, + 'description': self.cfg.nexenta_dataset_description, + 'driver_version': self.drv.VERSION, + 'storage_protocol': 'iSCSI', + 'total_capacity_gb': 10, + 'free_capacity_gb': 8, + 'reserved_percentage': self.cfg.reserved_percentage, + 'QoS_support': False, + 'volume_backend_name': self.drv.backend_name, + 'location_info': location_info, + 'iscsi_target_portal_port': ( + self.cfg.nexenta_iscsi_target_portal_port), + 'nef_url': self.drv.nef.url + } + self.drv._update_volume_stats() + self.assertEqual(stats, self.drv._stats) + + +class TestNexentaJSONProxy(test.TestCase): + + def __init__(self, method): + super(TestNexentaJSONProxy, self).__init__(method) + + @patch('requests.Response.close') + @patch('requests.get') + @patch('requests.post') + def test_call(self, post, get, close): + nef_get = jsonrpc.NexentaJSONProxy( + 'http', '1.1.1.1', '8080', 'user', 'pass', method='get') + nef_post = jsonrpc.NexentaJSONProxy( + 'http', '1.1.1.1', '8080', 'user', 'pass', method='post') + data = {'key': 'value'} + get.return_value = requests.Response() + post.return_value = requests.Response() + + get.return_value.__setstate__({ + 'status_code': 200, '_content': jsonutils.dumps(data)}) + self.assertEqual({'key': 'value'}, nef_get('url')) + + get.return_value.__setstate__({ + 'status_code': 201, '_content': ''}) + self.assertEqual('Success', nef_get('url')) + + data2 = {'links': [{'href': 'redirect_url'}]} + post.return_value.__setstate__({ + 'status_code': 202, '_content': jsonutils.dumps(data2)}) + get.return_value.__setstate__({ + 'status_code': 200, '_content': jsonutils.dumps(data)}) + self.assertEqual({'key': 'value'}, nef_post('url')) + + get.return_value.__setstate__({ + 'status_code': 200, '_content': ''}) + self.assertEqual('Success', nef_post('url', data)) + + get.return_value.__setstate__({ + 'status_code': 400, + '_content': jsonutils.dumps({'code': 'ENOENT'})}) + self.assertRaises(exception.NexentaException, lambda: nef_get('url')) diff --git a/cinder/volume/drivers/nexenta/ns5/iscsi.py b/cinder/volume/drivers/nexenta/ns5/iscsi.py index 2e82f0316..3faef5276 100644 --- a/cinder/volume/drivers/nexenta/ns5/iscsi.py +++ b/cinder/volume/drivers/nexenta/ns5/iscsi.py @@ -289,7 +289,7 @@ class NexentaISCSIDriver(driver.ISCSIDriver): # pylint: disable=R0921 try: self.nef.delete(url) except exception.NexentaException as exc: - if 'EBUSY' is exc: + if 'EBUSY' in exc.args[0]: LOG.warning(_LW( 'Could not delete snapshot %s - it has dependencies'), snapshot['name']) @@ -476,7 +476,13 @@ class NexentaISCSIDriver(driver.ISCSIDriver): # pylint: disable=R0921 targetgroup_name = self._get_targetgroup_name(volume) url = 'san/targetgroups/%s/luns/%s' % ( targetgroup_name, lun_id) - self.nef.delete(url) + try: + self.nef.delete(url) + except exception.NexentaException as exc: + if 'No such logical unit in target group' in exc.args[0]: + LOG.debug('LU already deleted from appliance') + else: + raise def get_volume_stats(self, refresh=False): """Get volume stats. @@ -497,9 +503,9 @@ class NexentaISCSIDriver(driver.ISCSIDriver): # pylint: disable=R0921 'group': self.volume_group, } stats = self.nef.get(url) - total_amount = utils.str2gib_size( - stats['bytesAvailable'] + stats['bytesUsed']) - free_amount = utils.str2gib_size(stats['bytesAvailable']) + total_amount = utils.str2gib_size(stats['bytesAvailable']) + free_amount = utils.str2gib_size( + stats['bytesAvailable'] - stats['bytesUsed']) location_info = '%(driver)s:%(host)s:%(pool)s/%(group)s' % { 'driver': self.__class__.__name__, diff --git a/cinder/volume/drivers/nexenta/ns5/jsonrpc.py b/cinder/volume/drivers/nexenta/ns5/jsonrpc.py index c37c49cdd..7689e7b4c 100644 --- a/cinder/volume/drivers/nexenta/ns5/jsonrpc.py +++ b/cinder/volume/drivers/nexenta/ns5/jsonrpc.py @@ -19,6 +19,8 @@ .. automodule:: nexenta.jsonrpc """ +import base64 +import json import time from oslo_log import log as logging @@ -59,7 +61,8 @@ class NexentaJSONProxy(object): return 'NEF proxy: %s' % self.url def __call__(self, path, data=None): - auth = ('%s:%s' % (self.user, self.password)).encode('base64')[:-1] + auth = base64.b64encode( + ('%s:%s' % (self.user, self.password)).encode('utf-8'))[:-1] headers = { 'Content-Type': 'application/json', 'Authorization': 'Basic %s' % auth @@ -71,21 +74,15 @@ class NexentaJSONProxy(object): LOG.debug('Sending JSON to url: %s, data: %s, method: %s', path, data, self.method) - if self.method == 'get': - resp = requests.get(url, headers=headers) - if self.method == 'post': - resp = requests.post(url, data=data, headers=headers) - if self.method == 'put': - resp = requests.put(url, data=data, headers=headers) - if self.method == 'delete': - resp = requests.delete(url, data=data, headers=headers) + + resp = getattr(requests, self.method)(url, data=data, headers=headers) if resp.status_code == 201 or ( resp.status_code == 200 and not resp.content): LOG.debug('Got response: Success') return 'Success' - response = resp.json() + response = json.loads(resp.content) resp.close() if response and resp.status_code == 202: url = self.url + response['links'][0]['href'] @@ -97,7 +94,7 @@ class NexentaJSONProxy(object): LOG.debug('Got response: Success') return 'Success' else: - response = resp.json() + response = json.loads(resp.content) resp.close() if response.get('code'): raise exception.NexentaException(response) -- 2.45.2