From a2d647126c21b2233e5d574143052070281062cc Mon Sep 17 00:00:00 2001 From: Nikolay Sobolevskiy Date: Thu, 1 Aug 2013 16:57:54 +0400 Subject: [PATCH] Implement missing Coraid Driver functionality for Havana Intorduce a brick connector for AoE protocol. Refactoring Coraid Driver. Implement missing functionality for Havana: - Copy Volume To Image - Copy Image To Volume - Clone Volume Fix a bug with resize volume command. Change-Id: I2af6a41dc44cb8bc4b74da752e0be9ed54a83cb1 Implements: blueprint coraid-driver-refactoring-for-havana --- cinder/brick/initiator/connector.py | 112 +++ cinder/exception.py | 28 + cinder/tests/brick/test_brick_connector.py | 103 +++ cinder/tests/test_coraid.py | 894 ++++++++++++++++----- cinder/volume/drivers/coraid.py | 712 ++++++++-------- etc/cinder/rootwrap.d/volume.filters | 5 + 6 files changed, 1347 insertions(+), 507 deletions(-) diff --git a/cinder/brick/initiator/connector.py b/cinder/brick/initiator/connector.py index 2560f7e7b..35f3775b7 100644 --- a/cinder/brick/initiator/connector.py +++ b/cinder/brick/initiator/connector.py @@ -100,6 +100,10 @@ class InitiatorConnector(executor.Executor): driver=driver, root_helper=root_helper, use_multipath=use_multipath) + elif protocol == "AOE": + return AoEConnector(execute=execute, + driver=driver, + root_helper=root_helper) else: msg = (_("Invalid InitiatorConnector protocol " "specified %(protocol)s") % @@ -650,3 +654,111 @@ class FibreChannelConnector(InitiatorConnector): pci_num = device_path[index - 1] return pci_num + + +class AoEConnector(InitiatorConnector): + """Connector class to attach/detach AoE volumes.""" + def __init__(self, driver=None, execute=putils.execute, + root_helper="sudo", *args, **kwargs): + super(AoEConnector, self).__init__(driver, execute, root_helper, + *args, **kwargs) + + def _get_aoe_info(self, connection_properties): + shelf = connection_properties['target_shelf'] + lun = connection_properties['target_lun'] + aoe_device = 'e%(shelf)s.%(lun)s' % {'shelf': shelf, + 'lun': lun} + aoe_path = '/dev/etherd/%s' % (aoe_device) + return aoe_device, aoe_path + + @lockutils.synchronized('aoe_control', 'aoe-') + def connect_volume(self, connection_properties): + """Discover and attach the volume. + + connection_properties for AoE must include: + target_shelf - shelf id of volume + target_lun - lun id of volume + """ + aoe_device, aoe_path = self._get_aoe_info(connection_properties) + + device_info = { + 'type': 'block', + 'device': aoe_device, + 'path': aoe_path, + } + + if os.path.exists(aoe_path): + self._aoe_revalidate(aoe_device) + else: + self._aoe_discover() + + waiting_status = {'tries': 0} + + #NOTE(jbr_): Device path is not always present immediately + def _wait_for_discovery(aoe_path): + if os.path.exists(aoe_path): + raise loopingcall.LoopingCallDone + + if waiting_status['tries'] >= CONF.num_volume_device_scan_tries: + raise exception.VolumeDeviceNotFound(device=aoe_path) + + LOG.warn(_("AoE volume not yet found at: %(path)s. " + "Try number: %(tries)s"), + {'path': aoe_device, + 'tries': waiting_status['tries']}) + + self._aoe_discover() + waiting_status['tries'] += 1 + + timer = loopingcall.FixedIntervalLoopingCall(_wait_for_discovery, + aoe_path) + timer.start(interval=2).wait() + + if waiting_status['tries']: + LOG.debug(_("Found AoE device %(path)s " + "(after %(tries)s rediscover)"), + {'path': aoe_path, + 'tries': waiting_status['tries']}) + + return device_info + + @lockutils.synchronized('aoe_control', 'aoe-') + def disconnect_volume(self, connection_properties, device_info): + """Detach and flush the volume. + + connection_properties for AoE must include: + target_shelf - shelf id of volume + target_lun - lun id of volume + """ + aoe_device, aoe_path = self._get_aoe_info(connection_properties) + + if os.path.exists(aoe_path): + self._aoe_flush(aoe_device) + + def _aoe_discover(self): + (out, err) = self._execute('aoe-discover', + run_as_root=True, + root_helper=self._root_helper, + check_exit_code=0) + + LOG.debug(_('aoe-discover: stdout=%(out)s stderr%(err)s') % + {'out': out, 'err': err}) + + def _aoe_revalidate(self, aoe_device): + (out, err) = self._execute('aoe-revalidate', + aoe_device, + run_as_root=True, + root_helper=self._root_helper, + check_exit_code=0) + + LOG.debug(_('aoe-revalidate %(dev)s: stdout=%(out)s stderr%(err)s') % + {'dev': aoe_device, 'out': out, 'err': err}) + + def _aoe_flush(self, aoe_device): + (out, err) = self._execute('aoe-flush', + aoe_device, + run_as_root=True, + root_helper=self._root_helper, + check_exit_code=0) + LOG.debug(_('aoe-flush %(dev)s: stdout=%(out)s stderr%(err)s') % + {'dev': aoe_device, 'out': out, 'err': err}) diff --git a/cinder/exception.py b/cinder/exception.py index ef5261fdb..9bf902fde 100644 --- a/cinder/exception.py +++ b/cinder/exception.py @@ -618,3 +618,31 @@ class ProtocolNotSupported(CinderException): class SSHInjectionThreat(CinderException): message = _("SSH command injection detected") + ": %(command)s" + + +class CoraidException(CinderException): + message = _('Coraid Cinder Driver exception.') + + +class CoraidJsonEncodeFailure(CoraidException): + message = _('Failed to encode json data.') + + +class CoraidESMBadCredentials(CoraidException): + message = _('Login on ESM failed.') + + +class CoraidESMReloginFailed(CoraidException): + message = _('Relogin on ESM failed.') + + +class CoraidESMBadGroup(CoraidException): + message = _('Group with name "%(group_name)s" not found.') + + +class CoraidESMConfigureError(CoraidException): + message = _('ESM configure request failed: %(message)s.') + + +class CoraidESMNotAvailable(CoraidException): + message = _('Coraid ESM not available with reason: %(reason)s.') diff --git a/cinder/tests/brick/test_brick_connector.py b/cinder/tests/brick/test_brick_connector.py index f523d89af..5ec9e657e 100644 --- a/cinder/tests/brick/test_brick_connector.py +++ b/cinder/tests/brick/test_brick_connector.py @@ -18,6 +18,8 @@ import os.path import string import time +import mox + from cinder.brick import exception from cinder.brick.initiator import connector from cinder.brick.initiator import host_driver @@ -60,6 +62,10 @@ class ConnectorTestCase(test.TestCase): self.assertTrue(obj.__class__.__name__, "FibreChannelConnector") + obj = connector.InitiatorConnector.factory('aoe') + self.assertTrue(obj.__class__.__name__, + "AoEConnector") + self.assertRaises(ValueError, connector.InitiatorConnector.factory, "bogus") @@ -322,3 +328,100 @@ class FibreChannelConnectorTestCase(ConnectorTestCase): self.assertRaises(exception.NoFibreChannelHostsFound, self.connector.connect_volume, connection_info['data']) + + +class AoEConnectorTestCase(ConnectorTestCase): + """Test cases for AoE initiator class.""" + def setUp(self): + super(AoEConnectorTestCase, self).setUp() + self.mox = mox.Mox() + self.connector = connector.AoEConnector() + self.connection_properties = {'target_shelf': 'fake_shelf', + 'target_lun': 'fake_lun'} + + def tearDown(self): + self.mox.VerifyAll() + self.mox.UnsetStubs() + super(AoEConnectorTestCase, self).tearDown() + + def _mock_path_exists(self, aoe_path, mock_values=[]): + self.mox.StubOutWithMock(os.path, 'exists') + for value in mock_values: + os.path.exists(aoe_path).AndReturn(value) + + def test_connect_volume(self): + """Ensure that if path exist aoe-revaliadte was called.""" + aoe_device, aoe_path = self.connector._get_aoe_info( + self.connection_properties) + + self._mock_path_exists(aoe_path, [True, True]) + + self.mox.StubOutWithMock(self.connector, '_execute') + self.connector._execute('aoe-revalidate', + aoe_device, + run_as_root=True, + root_helper='sudo', + check_exit_code=0).AndReturn(("", "")) + self.mox.ReplayAll() + + self.connector.connect_volume(self.connection_properties) + + def test_connect_volume_without_path(self): + """Ensure that if path doesn't exist aoe-discovery was called.""" + + aoe_device, aoe_path = self.connector._get_aoe_info( + self.connection_properties) + expected_info = { + 'type': 'block', + 'device': aoe_device, + 'path': aoe_path, + } + + self._mock_path_exists(aoe_path, [False, True]) + + self.mox.StubOutWithMock(self.connector, '_execute') + self.connector._execute('aoe-discover', + run_as_root=True, + root_helper='sudo', + check_exit_code=0).AndReturn(("", "")) + self.mox.ReplayAll() + + volume_info = self.connector.connect_volume( + self.connection_properties) + + self.assertDictMatch(volume_info, expected_info) + + def test_connect_volume_could_not_discover_path(self): + aoe_device, aoe_path = self.connector._get_aoe_info( + self.connection_properties) + + number_of_calls = 4 + self._mock_path_exists(aoe_path, [False] * (number_of_calls + 1)) + self.mox.StubOutWithMock(self.connector, '_execute') + + for i in xrange(number_of_calls): + self.connector._execute('aoe-discover', + run_as_root=True, + root_helper='sudo', + check_exit_code=0).AndReturn(("", "")) + self.mox.ReplayAll() + self.assertRaises(exception.VolumeDeviceNotFound, + self.connector.connect_volume, + self.connection_properties) + + def test_disconnect_volume(self): + """Ensure that if path exist aoe-revaliadte was called.""" + aoe_device, aoe_path = self.connector._get_aoe_info( + self.connection_properties) + + self._mock_path_exists(aoe_path, [True]) + + self.mox.StubOutWithMock(self.connector, '_execute') + self.connector._execute('aoe-flush', + aoe_device, + run_as_root=True, + root_helper='sudo', + check_exit_code=0).AndReturn(("", "")) + self.mox.ReplayAll() + + self.connector.disconnect_volume(self.connection_properties, {}) diff --git a/cinder/tests/test_coraid.py b/cinder/tests/test_coraid.py index e2cb275c9..f31b85084 100644 --- a/cinder/tests/test_coraid.py +++ b/cinder/tests/test_coraid.py @@ -15,30 +15,44 @@ # License for the specific language governing permissions and limitations # under the License. +import math + import mox +from cinder.brick.initiator import connector from cinder import exception +from cinder.image import image_utils +from cinder.openstack.common import jsonutils from cinder.openstack.common import log as logging from cinder import test +from cinder import units from cinder.volume import configuration as conf from cinder.volume.drivers import coraid -from cinder.volume.drivers.coraid import CoraidDriver -from cinder.volume.drivers.coraid import CoraidESMException -from cinder.volume.drivers.coraid import CoraidRESTClient +from cinder.volume import volume_types -import cookielib -import urllib2 LOG = logging.getLogger(__name__) +def to_coraid_kb(gb): + return math.ceil(float(gb) * units.GiB / 1000) + + +def coraid_volume_size(gb): + return '{0}K'.format(to_coraid_kb(gb)) + + fake_esm_ipaddress = "192.168.0.1" fake_esm_username = "darmok" fake_esm_group = "tanagra" +fake_esm_group_id = 1 fake_esm_password = "12345678" +fake_coraid_repository_key = 'repository_key' + fake_volume_name = "volume-12345678-1234-1234-1234-1234567890ab" -fake_volume_size = "10" +fake_clone_name = "volume-ffffffff-1234-1234-1234-1234567890ab" +fake_volume_size = 10 fake_repository_name = "A-B:C:D" fake_pool_name = "FakePool" fake_aoetarget = 4081 @@ -48,9 +62,19 @@ fake_lun = 241 fake_str_aoetarget = str(fake_aoetarget) fake_lun_addr = {"shelf": fake_shelf, "lun": fake_lun} +fake_volume_type = {'id': 1} + fake_volume = {"name": fake_volume_name, "size": fake_volume_size, - "volume_type": {"id": 1}} + "volume_type": fake_volume_type} + +fake_clone_volume = {"name": fake_clone_name, + "size": fake_volume_size, + "volume_type": fake_volume_type} + +fake_big_clone_volume = {"name": fake_clone_name, + "size": fake_volume_size + 1, + "volume_type": fake_volume_type} fake_volume_info = {"pool": fake_pool_name, "repo": fake_repository_name, @@ -64,13 +88,15 @@ fake_snapshot_name = "snapshot-12345678-8888-8888-1234-1234567890ab" fake_snapshot_id = "12345678-8888-8888-1234-1234567890ab" fake_volume_id = "12345678-1234-1234-1234-1234567890ab" fake_snapshot = {"id": fake_snapshot_id, + "name": fake_snapshot_name, "volume_id": fake_volume_id, - "volume_size": 10} + "volume_name": fake_volume_name, + "volume_size": int(fake_volume_size) - 1} fake_configure_data = [{"addr": "cms", "data": "FAKE"}] fake_esm_fetch = [[ - {"command": "super_fake_command_of_death"}, + {"command": "super_fake_command"}, {"reply": [ {"lv": {"containingPool": fake_pool_name, @@ -83,6 +109,10 @@ fake_esm_fetch = [[ }, "repoName": fake_repository_name}]}]] +fake_esm_fetch_no_volume = [[ + {"command": "super_fake_command"}, + {"reply": []}]] + fake_esm_success = {"category": "provider", "tracking": False, "configState": "completedSuccessfully", @@ -109,12 +139,97 @@ fake_login_reply_group_fail = {"values": [ "metaCROp": "noAction"} -class TestCoraidDriver(test.TestCase): +def compare(a, b): + if type(a) != type(b): + return False + if type(a) == list or type(a) == tuple: + if len(a) != len(b): + return False + return all(map(lambda t: compare(t[0], t[1]), zip(a, b))) + elif type(a) == dict: + if len(a) != len(b): + return False + for k, v in a.items(): + if not compare(v, b[k]): + return False + return True + else: + return a == b + + +def pack_data(request): + request['data'] = jsonutils.dumps(request['data']) + + +class FakeRpcBadRequest(Exception): + pass + + +class FakeRpcIsNotCalled(Exception): + def __init__(self, handle, url_params, data): + self.handle = handle + self.url_params = url_params + self.data = data + + def __str__(self): + return 'Fake Rpc handle for {0}/{1}/{2} not found'.format( + self.handle, self.url_params, self.data) + + +class FakeRpcHandle(object): + def __init__(self, handle, url_params, data, result): + self.handle = handle + self.url_params = url_params + self.data = data + self.result = result + self._is_called = False + + def set_called(self): + self._is_called = True + + def __call__(self, handle, url_params, data, + allow_empty_response=False): + if handle != self.handle: + raise FakeRpcBadRequest( + 'Unexpected handle name {0}. Expected {1}.' + .format(handle, self.handle)) + if not compare(url_params, self.url_params): + raise FakeRpcBadRequest('Unexpected url params: {0} / {1}' + .format(url_params, self.url_params)) + if not compare(data, self.data): + raise FakeRpcBadRequest('Unexpected data: {0}/{1}' + .format(data, self.data)) + if callable(self.result): + return self.result() + else: + return self.result + + +class FakeRpc(object): + def __init__(self): + self._handles = [] + + def handle(self, handle, url_params, data, result): + self._handles.append(FakeRpcHandle(handle, url_params, data, result)) + + def __call__(self, handle_name, url_params, data, + allow_empty_response=False): + for handle in self._handles: + if (handle.handle == handle_name and + compare(handle.url_params, url_params) and + compare(handle.data, handle.data)): + handle.set_called() + return handle(handle_name, url_params, data, + allow_empty_response) + raise FakeRpcIsNotCalled(handle_name, url_params, data) + + +class CoraidDriverTestCase(test.TestCase): def setUp(self): - super(TestCoraidDriver, self).setUp() - self.esm_mock = self.mox.CreateMockAnything() - self.stubs.Set(coraid, 'CoraidRESTClient', - lambda *_, **__: self.esm_mock) + super(CoraidDriverTestCase, self).setUp() + + self.mox = mox.Mox() + configuration = mox.MockObject(conf.Configuration) configuration.append_config_values(mox.IgnoreArg()) configuration.coraid_esm_address = fake_esm_ipaddress @@ -123,204 +238,595 @@ class TestCoraidDriver(test.TestCase): configuration.coraid_password = fake_esm_password configuration.volume_name_template = "volume-%s" configuration.snapshot_name_template = "snapshot-%s" + configuration.coraid_repository_key = fake_coraid_repository_key + configuration.use_multipath_for_image_xfer = False + self.fake_rpc = FakeRpc() + + self.mox.StubOutWithMock(coraid.CoraidRESTClient, 'rpc') + coraid.CoraidRESTClient.rpc = self.fake_rpc + + self.driver = coraid.CoraidDriver(configuration=configuration) + self.driver.do_setup({}) + + def tearDown(self): + self.mox.UnsetStubs() + super(CoraidDriverTestCase, self).tearDown() + + def mock_volume_types(self, repositories=[]): + if not repositories: + repositories = [fake_repository_name] + self.mox.StubOutWithMock(volume_types, 'get_volume_type_extra_specs') + for repository in repositories: + (volume_types + .get_volume_type_extra_specs(fake_volume_type['id'], + fake_coraid_repository_key) + .AndReturn(' {0}'.format(repository))) + + +class CoraidDriverLoginSuccessTestCase(CoraidDriverTestCase): + def setUp(self): + super(CoraidDriverLoginSuccessTestCase, self).setUp() + + login_results = {'state': 'adminSucceed', + 'values': [ + {'fullPath': + 'admin group:{0}'.format(fake_esm_group), + 'groupId': fake_esm_group_id + }]} + + self.fake_rpc.handle('admin', {'op': 'login', + 'username': fake_esm_username, + 'password': fake_esm_password}, + 'Login', login_results) + + self.fake_rpc.handle('admin', {'op': 'setRbacGroup', + 'groupId': fake_esm_group_id}, + 'Group', {'state': 'adminSucceed'}) + + +class CoraidDriverApplianceTestCase(CoraidDriverLoginSuccessTestCase): + def test_resize_volume(self): + new_volume_size = int(fake_volume_size) + 1 + + fetch_request = {'shelf': 'cms', + 'orchStrRepo': '', + 'lv': fake_volume_name} + self.fake_rpc.handle('fetch', fetch_request, None, + fake_esm_fetch) + + reply = {'configState': 'completedSuccessfully'} - self.drv = CoraidDriver(configuration=configuration) - self.drv.do_setup({}) + resize_volume_request = {'addr': 'cms', + 'data': { + 'lvName': fake_volume_name, + 'newLvName': fake_volume_name + '-resize', + 'size': + coraid_volume_size(new_volume_size), + 'repoName': fake_repository_name}, + 'op': 'orchStrLunMods', + 'args': 'resize'} + pack_data(resize_volume_request) + self.fake_rpc.handle('configure', {}, [resize_volume_request], + reply) + real_reply = self.driver._appliance.resize_volume(fake_volume_name, + new_volume_size) + + self.assertEqual(reply['configState'], real_reply['configState']) + + +class CoraidDriverIntegrationalTestCase(CoraidDriverLoginSuccessTestCase): def test_create_volume(self): - setattr(self.esm_mock, 'create_lun', lambda *_: True) - self.stubs.Set(CoraidDriver, '_get_repository', - lambda *_: fake_repository_name) - self.drv.create_volume(fake_volume) + self.mock_volume_types() + + create_volume_request = {'addr': 'cms', + 'data': { + 'servers': [], + 'size': + coraid_volume_size(fake_volume_size), + 'repoName': fake_repository_name, + 'lvName': fake_volume_name}, + 'op': 'orchStrLun', + 'args': 'add'} + pack_data(create_volume_request) + + self.fake_rpc.handle('configure', {}, [create_volume_request], + {'configState': 'completedSuccessfully', + 'firstParam': 'fake_first_param'}) + + self.mox.ReplayAll() + + self.driver.create_volume(fake_volume) + + self.mox.VerifyAll() def test_delete_volume(self): - setattr(self.esm_mock, 'delete_lun', - lambda *_: True) - self.drv.delete_volume(fake_volume) + delete_volume_request = {'addr': 'cms', + 'data': { + 'repoName': fake_repository_name, + 'lvName': fake_volume_name}, + 'op': 'orchStrLun/verified', + 'args': 'delete'} + pack_data(delete_volume_request) - def test_initialize_connection(self): - setattr(self.esm_mock, '_get_lun_address', - lambda *_: fake_lun_addr) - self.drv.initialize_connection(fake_volume, '') + self.fake_rpc.handle('configure', {}, [delete_volume_request], + {'configState': 'completedSuccessfully'}) + + self.fake_rpc.handle('fetch', {'orchStrRepo': '', + 'shelf': 'cms', + 'lv': fake_volume_name}, + None, + fake_esm_fetch) + + self.mox.ReplayAll() + + self.driver.delete_volume(fake_volume) + + self.mox.VerifyAll() + + def test_ping_ok(self): + self.fake_rpc.handle('fetch', {}, None, '') + + self.mox.ReplayAll() + + self.driver._appliance.ping() + + self.mox.VerifyAll() + + def test_ping_failed(self): + self.mox.StubOutWithMock(self.driver._appliance, 'rpc') + + def rpc(handle, url_params, data, + allow_empty_response=True): + raise Exception("Some exception") + + self.driver._appliance.rpc = rpc + + self.mox.ReplayAll() + + self.assertRaises(exception.CoraidESMNotAvailable, + self.driver._appliance.ping) + + self.mox.VerifyAll() + + def test_delete_not_existing_lun(self): + delete_volume_request = {'addr': 'cms', + 'data': { + 'repoName': fake_repository_name, + 'lvName': fake_volume_name}, + 'op': 'orchStrLun/verified', + 'args': 'delete'} + pack_data(delete_volume_request) + + self.fake_rpc.handle('configure', {}, [delete_volume_request], + {'configState': 'completedSuccessfully'}) + + self.fake_rpc.handle('fetch', {'orchStrRepo': '', + 'shelf': 'cms', + 'lv': fake_volume_name}, + None, + fake_esm_fetch_no_volume) + + self.mox.ReplayAll() + + self.assertRaises( + exception.VolumeNotFound, + self.driver._appliance.delete_lun, + fake_volume['name']) + + self.mox.VerifyAll() + + def test_delete_not_existing_volume_appliance_is_ok(self): + self.mox.StubOutWithMock(self.driver._appliance, 'delete_lun') + + def delete_lun(volume_name): + raise exception.VolumeNotFound(volume_id=fake_volume['name']) + + self.driver._appliance.delete_lun = delete_lun + + self.mox.StubOutWithMock(self.driver._appliance, 'ping') + + def ping(): + pass + + self.driver._appliance.ping = ping + + self.mox.ReplayAll() + + self.driver.delete_volume(fake_volume) + + self.mox.VerifyAll() + + def test_delete_not_existing_volume_sleeping_appliance(self): + self.mox.StubOutWithMock(self.driver._appliance, 'delete_lun') + + def delete_lun(volume_name): + raise exception.VolumeNotFound(volume_id=fake_volume['name']) + + self.driver._appliance.delete_lun = delete_lun + + self.mox.StubOutWithMock(self.driver._appliance, 'ping') + + def ping(): + raise exception.CoraidESMNotAvailable(reason="Any reason") + + self.driver._appliance.ping = ping + + self.mox.ReplayAll() + + self.assertRaises(exception.CoraidESMNotAvailable, + self.driver.delete_volume, + fake_volume) + + self.mox.VerifyAll() def test_create_snapshot(self): - setattr(self.esm_mock, 'create_snapshot', - lambda *_: True) - self.drv.create_snapshot(fake_snapshot) + fetch_request = {'shelf': 'cms', + 'orchStrRepo': '', + 'lv': fake_volume_name} + self.fake_rpc.handle('fetch', fetch_request, None, + fake_esm_fetch) + + create_snapshot_request = {'addr': 'cms', + 'data': { + 'repoName': fake_repository_name, + 'lvName': fake_volume_name, + 'newLvName': fake_snapshot_name}, + 'op': 'orchStrLunMods', + 'args': 'addClSnap'} + pack_data(create_snapshot_request) + self.fake_rpc.handle('configure', {}, [create_snapshot_request], + {'configState': 'completedSuccessfully'}) + + self.mox.ReplayAll() + + self.driver.create_snapshot(fake_snapshot) + + self.mox.VerifyAll() def test_delete_snapshot(self): - setattr(self.esm_mock, 'delete_snapshot', - lambda *_: True) - self.drv.delete_snapshot(fake_snapshot) + fetch_request = {'shelf': 'cms', + 'orchStrRepo': '', + 'lv': fake_snapshot_name} + self.fake_rpc.handle('fetch', fetch_request, None, + fake_esm_fetch) + + delete_snapshot_request = {'addr': 'cms', + 'data': { + 'repoName': fake_repository_name, + 'lvName': fake_snapshot_name}, + 'op': 'orchStrLunMods', + 'args': 'delClSnap'} + pack_data(delete_snapshot_request) + self.fake_rpc.handle('configure', {}, [delete_snapshot_request], + {'configState': 'completedSuccessfully'}) + + self.mox.ReplayAll() + + self.driver.delete_snapshot(fake_snapshot) + + self.mox.VerifyAll() def test_create_volume_from_snapshot(self): - self.esm_mock.create_volume_from_snapshot( - fake_volume, - fake_snapshot).AndReturn(True) - mox.Replay(self.esm_mock) - self.esm_mock.create_volume_from_snapshot(fake_volume, fake_snapshot) - mox.Verify(self.esm_mock) - - def test_create_volume_from_snapshot_bigger(self): - self.esm_mock.create_volume_from_snapshot( - fake_volume, - fake_snapshot).AndReturn(True) - self.esm_mock.resize_volume(fake_volume_name, - '20').AndReturn(True) - mox.Replay(self.esm_mock) - self.esm_mock.create_volume_from_snapshot(fake_volume, fake_snapshot) - self.esm_mock.resize_volume(fake_volume_name, '20') - mox.Verify(self.esm_mock) + self.mock_volume_types() + + self.mox.StubOutWithMock(self.driver._appliance, 'resize_volume') + self.driver._appliance.resize_volume(fake_volume_name, + fake_volume['size'])\ + .AndReturn(None) + + fetch_request = {'shelf': 'cms', + 'orchStrRepo': '', + 'lv': fake_snapshot_name} + self.fake_rpc.handle('fetch', fetch_request, None, + fake_esm_fetch) + + create_clone_request = {'addr': 'cms', + 'data': { + 'lvName': fake_snapshot_name, + 'repoName': fake_repository_name, + 'newLvName': fake_volume_name, + 'newRepoName': fake_repository_name}, + 'op': 'orchStrLunMods', + 'args': 'addClone'} + pack_data(create_clone_request) + self.fake_rpc.handle('configure', {}, [create_clone_request], + {'configState': 'completedSuccessfully'}) + + self.mox.ReplayAll() + + self.driver.create_volume_from_snapshot(fake_volume, fake_snapshot) + + self.mox.VerifyAll() + + def test_initialize_connection(self): + fetch_request = {'shelf': 'cms', + 'orchStrRepo': '', + 'lv': fake_volume_name} + self.fake_rpc.handle('fetch', fetch_request, None, + fake_esm_fetch) + + self.mox.ReplayAll() + + connection = self.driver.initialize_connection(fake_volume, {}) + + self.mox.VerifyAll() + + self.assertEqual(connection['driver_volume_type'], 'aoe') + self.assertEqual(connection['data']['target_shelf'], fake_shelf) + self.assertEqual(connection['data']['target_lun'], fake_lun) + + def test_get_repository_capabilities(self): + reply = [[{}, {'reply': [ + {'name': 'repo1', + 'profile': + {'fullName': 'Bronze-Bronze:Profile1'}}, + {'name': 'repo2', + 'profile': + {'fullName': 'Bronze-Bronze:Profile2'}}]}]] + + self.fake_rpc.handle('fetch', {'orchStrRepo': ''}, None, + reply) + + self.mox.ReplayAll() + + capabilities = self.driver.get_volume_stats(refresh=True) + + self.mox.VerifyAll() + + self.assertEqual( + capabilities[fake_coraid_repository_key], + 'Bronze-Bronze:Profile1:repo1 Bronze-Bronze:Profile2:repo2') + + def test_create_cloned_volume(self): + self.mock_volume_types([fake_repository_name]) + + fetch_request = {'shelf': 'cms', + 'orchStrRepo': '', + 'lv': fake_volume_name} + self.fake_rpc.handle('fetch', fetch_request, None, + fake_esm_fetch) + + shelf_lun = '{0}.{1}'.format(fake_shelf, fake_lun) + create_clone_request = {'addr': 'cms', + 'data': { + 'shelfLun': shelf_lun, + 'lvName': fake_volume_name, + 'repoName': fake_repository_name, + 'newLvName': fake_clone_name, + 'newRepoName': fake_repository_name}, + 'op': 'orchStrLunMods', + 'args': 'addClone'} + pack_data(create_clone_request) + self.fake_rpc.handle('configure', {}, [create_clone_request], + {'configState': 'completedSuccessfully'}) + + self.mox.ReplayAll() + + self.driver.create_cloned_volume(fake_clone_volume, fake_volume) + + self.mox.VerifyAll() + + def test_create_cloned_volume_with_resize(self): + self.mock_volume_types([fake_repository_name]) + + self.mox.StubOutWithMock(self.driver._appliance, 'resize_volume') + self.driver._appliance.resize_volume(fake_big_clone_volume['name'], + fake_big_clone_volume['size'])\ + .AndReturn(None) + + fetch_request = {'shelf': 'cms', + 'orchStrRepo': '', + 'lv': fake_volume_name} + self.fake_rpc.handle('fetch', fetch_request, None, + fake_esm_fetch) + + shelf_lun = '{0}.{1}'.format(fake_shelf, fake_lun) + create_clone_request = {'addr': 'cms', + 'data': { + 'shelfLun': shelf_lun, + 'lvName': fake_volume_name, + 'repoName': fake_repository_name, + 'newLvName': fake_clone_name, + 'newRepoName': fake_repository_name}, + 'op': 'orchStrLunMods', + 'args': 'addClone'} + pack_data(create_clone_request) + self.fake_rpc.handle('configure', {}, [create_clone_request], + {'configState': 'completedSuccessfully'}) + + self.mox.ReplayAll() + + self.driver.create_cloned_volume(fake_big_clone_volume, fake_volume) + + self.mox.VerifyAll() + + def test_create_cloned_volume_in_different_repository(self): + self.mock_volume_types([fake_repository_name + '_another']) + + fetch_request = {'shelf': 'cms', + 'orchStrRepo': '', + 'lv': fake_volume_name} + self.fake_rpc.handle('fetch', fetch_request, None, + fake_esm_fetch) + + self.mox.ReplayAll() + + self.assertRaises( + exception.CoraidException, + self.driver.create_cloned_volume, + fake_clone_volume, + fake_volume) + + self.mox.VerifyAll() def test_extend_volume(self): - self.esm_mock.resize_volume(fake_volume_name, - '20').AndReturn(True) - mox.Replay(self.esm_mock) - self.esm_mock.resize_volume(fake_volume_name, '20') - mox.Verify(self.esm_mock) + self.mox.StubOutWithMock(self.driver._appliance, 'resize_volume') + self.driver._appliance.resize_volume(fake_volume_name, 10)\ + .AndReturn(None) + + self.mox.ReplayAll() + + self.driver.extend_volume(fake_volume, 10) + self.mox.VerifyAll() -class TestCoraidRESTClient(test.TestCase): + +class AutoReloginCoraidTestCase(test.TestCase): def setUp(self): - super(TestCoraidRESTClient, self).setUp() - self.stubs.Set(cookielib, 'CookieJar', lambda *_: True) - self.stubs.Set(urllib2, 'build_opener', lambda *_: True) - self.stubs.Set(urllib2, 'HTTPCookieProcessor', lambda *_: True) - self.stubs.Set(CoraidRESTClient, '_login', lambda *_: True) - self.rest_mock = self.mox.CreateMockAnything() - self.stubs.Set(coraid, 'CoraidRESTClient', - lambda *_, **__: self.rest_mock) - self.drv = CoraidRESTClient(fake_esm_ipaddress, - fake_esm_username, - fake_esm_group, - fake_esm_password) - - def test__get_group_id(self): - setattr(self.rest_mock, '_get_group_id', - lambda *_: True) - self.assertEquals(self.drv._get_group_id(fake_esm_group, - fake_login_reply), - fake_group_id) - - def test__set_group(self): - setattr(self.rest_mock, '_set_group', - lambda *_: fake_group_id) - self.stubs.Set(CoraidRESTClient, '_admin_esm_cmd', - lambda *_: fake_login_reply) - self.drv._set_group(fake_login_reply) - - def test__set_group_fails_no_group(self): - setattr(self.rest_mock, '_set_group', - lambda *_: False) - self.stubs.Set(CoraidRESTClient, '_admin_esm_cmd', - lambda *_: fake_login_reply_group_fail) - self.assertRaises(CoraidESMException, - self.drv._set_group, - fake_login_reply_group_fail) - - def test__configure(self): - setattr(self.rest_mock, '_configure', - lambda *_: True) - self.stubs.Set(CoraidRESTClient, '_esm_cmd', - lambda *_: fake_esm_success) - self.drv._configure(fake_configure_data) - - def test__get_volume_info(self): - setattr(self.rest_mock, '_get_volume_info', - lambda *_: fake_volume_info) - self.stubs.Set(CoraidRESTClient, '_esm_cmd', - lambda *_: fake_esm_fetch) - self.drv._get_volume_info(fake_volume_name) - - def test__get_lun_address(self): - setattr(self.rest_mock, '_get_lun_address', - lambda *_: fake_lun_info) - self.stubs.Set(CoraidRESTClient, '_get_volume_info', - lambda *_: fake_volume_info) - self.drv._get_lun_address(fake_volume_name) - - def test_create_lun(self): - setattr(self.rest_mock, 'create_lun', - lambda *_: True) - self.stubs.Set(CoraidRESTClient, '_configure', - lambda *_: fake_esm_success) - self.rest_mock.create_lun(fake_volume_name, '10', - fake_repository_name) - self.drv.create_lun(fake_volume_name, '10', - fake_repository_name) - - def test_delete_lun_ok(self): - """Test Delete Volume classic case.""" - setattr(self.rest_mock, 'delete_lun', - lambda *_: self.mox.CreateMockAnything()) - self.stubs.Set(CoraidRESTClient, '_get_volume_info', - lambda *_: fake_volume_info) - self.stubs.Set(CoraidRESTClient, '_configure', - lambda *_: fake_esm_success) - self.rest_mock.delete_lun(fake_volume_name) - result = self.drv.delete_lun(fake_volume_name) - self.assertTrue(result) - - def test_delete_lun_in_error(self): - """Test Delete Volume in Error State.""" - setattr(self.rest_mock, 'delete_lun', - lambda *_: self.mox.CreateMockAnything()) - self.stubs.Set(CoraidRESTClient, '_get_volume_info', - lambda *_: Exception) - self.stubs.Set(CoraidRESTClient, '_check_esm_alive', - lambda *_: True) - self.rest_mock.delete_lun(fake_volume_name) - result = self.drv.delete_lun(fake_volume_name) - self.assertTrue(result) - - def test_delete_lun_esm_unavailable(self): - """Test Delete Volume with ESM Unavailable.""" - setattr(self.rest_mock, 'delete_lun', - lambda *_: self.mox.CreateMockAnything()) - self.stubs.Set(CoraidRESTClient, '_get_volume_info', - lambda *_: Exception) - self.stubs.Set(CoraidRESTClient, '_check_esm_alive', - lambda *_: False) - self.rest_mock.delete_lun(fake_volume_name) - result = self.drv.delete_lun(fake_volume_name) - self.assertRaises(Exception, result) + super(AutoReloginCoraidTestCase, self).setUp() + self.mox = mox.Mox() - def test_create_snapshot(self): - setattr(self.rest_mock, 'create_snapshot', - lambda *_: True) - self.stubs.Set(CoraidRESTClient, '_get_volume_info', - lambda *_: fake_volume_info) - self.stubs.Set(CoraidRESTClient, '_configure', - lambda *_: fake_esm_success) - self.drv.create_snapshot(fake_volume_name, - fake_volume_name) + self.rest_client = coraid.CoraidRESTClient('https://fake') + self.appliance = coraid.CoraidAppliance(self.rest_client, + 'fake_username', + 'fake_password', + 'fake_group') - def test_delete_snapshot(self): - setattr(self.rest_mock, 'delete_snapshot', - lambda *_: True) - self.stubs.Set(CoraidRESTClient, '_get_volume_info', - lambda *_: fake_volume_info) - self.stubs.Set(CoraidRESTClient, '_configure', - lambda *_: fake_esm_success) - self.drv.delete_snapshot(fake_volume_name) + def tearDown(self): + self.mox.UnsetStubs() + super(AutoReloginCoraidTestCase, self).tearDown() - def test_create_volume_from_snapshot(self): - setattr(self.rest_mock, 'create_volume_from_snapshot', - lambda *_: True) - self.stubs.Set(CoraidRESTClient, '_get_volume_info', - lambda *_: fake_volume_info) - self.stubs.Set(CoraidRESTClient, '_configure', - lambda *_: fake_esm_success) - self.drv.create_volume_from_snapshot(fake_volume_name, - fake_volume_name, - fake_repository_name) + def _test_auto_relogin_fail(self, state): + self.mox.StubOutWithMock(self.rest_client, 'rpc') - def test_resize_volume(self): - setattr(self.rest_mock, 'resize_volume', - lambda *_: True) - self.stubs.Set(CoraidRESTClient, '_get_volume_info', - lambda *_: fake_volume_info) - self.stubs.Set(CoraidRESTClient, '_configure', - lambda *_: fake_esm_success) - self.drv.resize_volume(fake_volume_name, - '20') + self.rest_client.rpc('fake_handle', {}, None, False).\ + AndReturn({'state': state, + 'metaCROp': 'reboot'}) + + self.rest_client.rpc('fake_handle', {}, None, False).\ + AndReturn({'state': state, + 'metaCROp': 'reboot'}) + + self.rest_client.rpc('fake_handle', {}, None, False).\ + AndReturn({'state': state, + 'metaCROp': 'reboot'}) + + self.mox.StubOutWithMock(self.appliance, '_ensure_session') + self.appliance._ensure_session().AndReturn(None) + + self.mox.StubOutWithMock(self.appliance, '_relogin') + self.appliance._relogin().AndReturn(None) + self.appliance._relogin().AndReturn(None) + + self.mox.ReplayAll() + + self.assertRaises(exception.CoraidESMReloginFailed, + self.appliance.rpc, + 'fake_handle', {}, None, False) + + self.mox.VerifyAll() + + def test_auto_relogin_fail_admin(self): + self._test_auto_relogin_fail('GeneralAdminFailure') + + def test_auto_relogin_fail_inactivity(self): + self._test_auto_relogin_fail('passwordInactivityTimeout') + + def test_auto_relogin_fail_absolute(self): + self._test_auto_relogin_fail('passwordAbsoluteTimeout') + + def test_auto_relogin_success(self): + self.mox.StubOutWithMock(self.rest_client, 'rpc') + + self.rest_client.rpc('fake_handle', {}, None, False).\ + AndReturn({'state': 'GeneralAdminFailure', + 'metaCROp': 'reboot'}) + + self.rest_client.rpc('fake_handle', {}, None, False).\ + AndReturn({'state': 'ok'}) + + self.mox.StubOutWithMock(self.appliance, '_ensure_session') + self.appliance._ensure_session().AndReturn(None) + + self.mox.StubOutWithMock(self.appliance, '_relogin') + self.appliance._relogin().AndReturn(None) + + self.mox.ReplayAll() + + reply = self.appliance.rpc('fake_handle', {}, None, False) + + self.mox.VerifyAll() + + self.assertEqual(reply['state'], 'ok') + + +class CoraidDriverImageTestCases(CoraidDriverTestCase): + def setUp(self): + super(CoraidDriverImageTestCases, self).setUp() + + self.fake_dev_path = '/dev/ether/fake_dev' + + self.fake_connection = {'driver_volume_type': 'aoe', + 'data': {'target_shelf': fake_shelf, + 'target_lun': fake_lun}} + + self.fake_volume_info = { + 'shelf': self.fake_connection['data']['target_shelf'], + 'lun': self.fake_connection['data']['target_lun']} + + self.mox.StubOutWithMock(self.driver, 'initialize_connection') + self.driver.initialize_connection(fake_volume, {})\ + .AndReturn(self.fake_connection) + + self.mox.StubOutWithMock(self.driver, 'terminate_connection') + self.driver.terminate_connection(fake_volume, mox.IgnoreArg())\ + .AndReturn(None) + + self.mox.StubOutWithMock(connector, 'get_connector_properties') + connector.get_connector_properties().AndReturn({}) + + self.mox.StubOutWithMock(connector.InitiatorConnector, 'factory') + + aoe_initiator = self.mox.CreateMockAnything() + + connector.InitiatorConnector.factory('aoe', use_multipath=False)\ + .AndReturn(aoe_initiator) + + aoe_initiator\ + .connect_volume(self.fake_connection['data'])\ + .AndReturn({'path': self.fake_dev_path}) + + aoe_initiator.check_valid_device(self.fake_dev_path)\ + .AndReturn(True) + + aoe_initiator.disconnect_volume( + {'target_shelf': self.fake_volume_info['shelf'], + 'target_lun': self.fake_volume_info['lun']}, mox.IgnoreArg()) + + def test_copy_volume_to_image(self): + fake_image_service = 'fake-image-service' + fake_image_meta = 'fake-image-meta' + + self.mox.StubOutWithMock(image_utils, 'upload_volume') + image_utils.upload_volume({}, + fake_image_service, + fake_image_meta, + self.fake_dev_path) + + self.mox.ReplayAll() + self.driver.copy_volume_to_image({}, + fake_volume, + fake_image_service, + fake_image_meta) + + self.mox.VerifyAll() + + def test_copy_image_to_volume(self): + fake_image_service = 'fake-image-service' + fake_image_id = 'fake-image-id;' + + self.mox.StubOutWithMock(image_utils, 'fetch_to_raw') + image_utils.fetch_to_raw({}, + fake_image_service, + fake_image_id, + self.fake_dev_path) + + self.mox.ReplayAll() + + self.driver.copy_image_to_volume({}, + fake_volume, + fake_image_service, + fake_image_id) + + self.mox.VerifyAll() diff --git a/cinder/volume/drivers/coraid.py b/cinder/volume/drivers/coraid.py index 23a656a6e..354dd885a 100644 --- a/cinder/volume/drivers/coraid.py +++ b/cinder/volume/drivers/coraid.py @@ -18,17 +18,24 @@ Desc : Driver to store volumes on Coraid Appliances. Require : Coraid EtherCloud ESM, Coraid VSX and Coraid SRX. Author : Jean-Baptiste RANSY +Author : Alex Zasimov +Author : Nikolay Sobolevsky Contrib : Larry Matter """ import cookielib -import time +import math +import urllib import urllib2 +import urlparse from oslo.config import cfg +from cinder import exception from cinder.openstack.common import jsonutils +from cinder.openstack.common import lockutils from cinder.openstack.common import log as logging +from cinder import units from cinder.volume import driver from cinder.volume import volume_types @@ -57,391 +64,473 @@ CONF = cfg.CONF CONF.register_opts(coraid_opts) -class CoraidException(Exception): - def __init__(self, message=None, error=None): - super(CoraidException, self).__init__(message, error) +ESM_SESSION_EXPIRED_STATES = ['GeneralAdminFailure', + 'passwordInactivityTimeout', + 'passwordAbsoluteTimeout'] - def __str__(self): - return '%s: %s' % self.args +class CoraidRESTClient(object): + """Executes REST RPC requests on Coraid ESM EtherCloud Appliance.""" + + def __init__(self, esm_url): + self._check_esm_url(esm_url) + self._esm_url = esm_url + self._cookie_jar = cookielib.CookieJar() + self._url_opener = urllib2.build_opener( + urllib2.HTTPCookieProcessor(self._cookie_jar)) + + def _check_esm_url(self, esm_url): + splitted = urlparse.urlsplit(esm_url) + if splitted.scheme != 'https': + raise ValueError( + _('Invalid ESM url scheme "%s". Supported https only.') % + splitted.scheme) + + @lockutils.synchronized('coraid_rpc', 'cinder-', False) + def rpc(self, handle, url_params, data, allow_empty_response=False): + return self._rpc(handle, url_params, data, allow_empty_response) + + def _rpc(self, handle, url_params, data, allow_empty_response): + """Execute REST RPC using url /handle?url_params. + + Send JSON encoded data in body of POST request. + + Exceptions: + urllib2.URLError + 1. Name or service not found (e.reason is socket.gaierror) + 2. Socket blocking operation timeout (e.reason is + socket.timeout) + 3. Network IO error (e.reason is socket.error) + + urllib2.HTTPError + 1. HTTP 404, HTTP 500 etc. + + CoraidJsonEncodeFailure - bad REST response + """ + # Handle must be simple path, for example: + # /configure + if '?' in handle or '&' in handle: + raise ValueError(_('Invalid REST handle name. Expected path.')) + + # Request url includes base ESM url, handle path and optional + # URL params. + rest_url = urlparse.urljoin(self._esm_url, handle) + encoded_url_params = urllib.urlencode(url_params) + if encoded_url_params: + rest_url += '?' + encoded_url_params + + if data is None: + json_request = None + else: + json_request = jsonutils.dumps(data) + + request = urllib2.Request(rest_url, json_request) + response = self._url_opener.open(request).read() -class CoraidRESTException(CoraidException): - pass + try: + if not response and allow_empty_response: + reply = {} + else: + reply = jsonutils.loads(response) + except (TypeError, ValueError) as exc: + msg = (_('Call to json.loads() failed: %(ex)s.' + ' Response: %(resp)s') % + {'ex': exc, 'resp': response}) + raise exception.CoraidJsonEncodeFailure(msg) + return reply -class CoraidESMException(CoraidException): - pass +def to_coraid_kb(gb): + return math.ceil(float(gb) * units.GiB / 1000) -class CoraidRESTClient(object): - """Executes volume driver commands on Coraid ESM EtherCloud Appliance.""" - - def __init__(self, ipaddress, user, group, password): - self.url = "https://%s:8443/" % ipaddress - self.user = user - self.group = group - self.password = password - self.session = False - self.cookiejar = cookielib.CookieJar() - self.urlOpener = urllib2.build_opener( - urllib2.HTTPCookieProcessor(self.cookiejar)) - LOG.debug(_('Running with CoraidDriver for ESM EtherCLoud')) + +def coraid_volume_size(gb): + return '{0}K'.format(to_coraid_kb(gb)) + + +class CoraidAppliance(object): + def __init__(self, rest_client, username, password, group): + self._rest_client = rest_client + self._username = username + self._password = password + self._group = group + self._logined = False def _login(self): - """Login and Session Handler.""" - if not self.session or self.session < time.time(): - url = ('admin?op=login&username=%s&password=%s' % - (self.user, self.password)) - data = 'Login' - reply = self._admin_esm_cmd(url, data) - if reply.get('state') == 'adminSucceed': - self.session = time.time() + 1100 - msg = _('Update session cookie %(session)s') - LOG.debug(msg % dict(session=self.session)) - self._set_group(reply) - return True - else: - errmsg = reply.get('message', '') - msg = _('Message : %(message)s') - raise CoraidESMException(msg % dict(message=errmsg)) - return True - - def _set_group(self, reply): - """Set effective group.""" - if self.group: - group = self.group - groupId = self._get_group_id(group, reply) - if groupId: - url = ('admin?op=setRbacGroup&groupId=%s' % (groupId)) - data = 'Group' - reply = self._admin_esm_cmd(url, data) - if reply.get('state') == 'adminSucceed': - return True - else: - errmsg = reply.get('message', '') - msg = _('Error while trying to set group: %(message)s') - raise CoraidRESTException(msg % dict(message=errmsg)) - else: - msg = _('Unable to find group: %(group)s') - raise CoraidESMException(msg % dict(group=group)) - return True - - def _get_group_id(self, groupName, loginResult): - """Map group name to group ID.""" - # NOTE(lmatter): All other groups are under the admin group - fullName = "admin group:%s" % groupName - groupId = False - for kid in loginResult['values']: - fullPath = kid['fullPath'] - if fullPath == fullName: - return kid['groupId'] - return False - - def _esm_cmd(self, url=False, data=None): - self._login() - return self._admin_esm_cmd(url, data) - - def _admin_esm_cmd(self, url=False, data=None): - """ - _admin_esm_cmd represent the entry point to send requests to ESM - Appliance. Send the HTTPS call, get response in JSON - convert response into Python Object and return it. + """Login into ESM. + + Perform login request and return available groups. + + :returns: dict -- map with group_name to group_id """ - if url: - url = self.url + url + ADMIN_GROUP_PREFIX = 'admin group:' - req = urllib2.Request(url, data) + url_params = {'op': 'login', + 'username': self._username, + 'password': self._password} + reply = self._rest_client.rpc('admin', url_params, 'Login') + if reply['state'] != 'adminSucceed': + raise exception.CoraidESMBadCredentials() - try: - res = self.urlOpener.open(req).read() - except Exception: - raise CoraidRESTException(_('ESM urlOpen error')) + # Read groups map from login reply. + groups_map = {} + for group_info in reply.get('values', []): + full_group_name = group_info['fullPath'] + if full_group_name.startswith(ADMIN_GROUP_PREFIX): + group_name = full_group_name[len(ADMIN_GROUP_PREFIX):] + groups_map[group_name] = group_info['groupId'] - try: - res_json = jsonutils.loads(res) - except Exception: - raise CoraidRESTException(_('JSON Error')) + return groups_map - return res_json - else: - raise CoraidRESTException(_('Request without URL')) + def _set_effective_group(self, groups_map, group): + """Set effective group. - def _check_esm_alive(self): + Use groups_map returned from _login method. + """ try: - url = self.url + 'fetch' - req = urllib2.Request(url) - code = self.urlOpener.open(req).getcode() - if code == '200': - return True - return False - except Exception: - return False - - def _configure(self, data): - """In charge of all commands into 'configure'.""" - url = 'configure' - LOG.debug(_('Configure data : %s'), data) - response = self._esm_cmd(url, data) - LOG.debug(_("Configure response : %s"), response) - if response: - if response.get('configState') == 'completedSuccessfully': - return True + group_id = groups_map[group] + except KeyError: + raise exception.CoraidESMBadGroup(group_name=group) + + url_params = {'op': 'setRbacGroup', + 'groupId': group_id} + reply = self._rest_client.rpc('admin', url_params, 'Group') + if reply['state'] != 'adminSucceed': + raise exception.CoraidESMBadCredentials() + + self._logined = True + + def _ensure_session(self): + if not self._logined: + groups_map = self._login() + self._set_effective_group(groups_map, self._group) + + def _relogin(self): + self._logined = False + self._ensure_session() + + def rpc(self, handle, url_params, data, allow_empty_response=False): + self._ensure_session() + + relogin_attempts = 3 + # Do action, relogin if needed and repeat action. + while True: + reply = self._rest_client.rpc(handle, url_params, data, + allow_empty_response) + + if ('state' in reply and + reply['state'] in ESM_SESSION_EXPIRED_STATES and + reply['metaCROp'] == 'reboot'): + relogin_attempts -= 1 + if relogin_attempts <= 0: + raise exception.CoraidESMReloginFailed() + LOG.debug(_('Session is expired. Relogin on ESM.')) + self._relogin() else: - errmsg = response.get('message', '') - msg = _('Message : %(message)s') - raise CoraidESMException(msg % dict(message=errmsg)) - return False - - def _get_volume_info(self, volume_name): - """Retrive volume informations for a given volume name.""" - url = 'fetch?shelf=cms&orchStrRepo&lv=%s' % (volume_name) + return reply + + def _is_bad_config_state(self, reply): + return (not reply or + 'configState' not in reply or + reply['configState'] != 'completedSuccessfully') + + def configure(self, json_request): + reply = self.rpc('configure', {}, json_request) + if self._is_bad_config_state(reply): + # Calculate error message + if not reply: + message = _('Reply is empty.') + else: + message = reply.get('message', _('Error message is empty.')) + raise exception.CoraidESMConfigureError(message=message) + return reply + + def esm_command(self, request): + request['data'] = jsonutils.dumps(request['data']) + return self.configure([request]) + + def get_volume_info(self, volume_name): + """Retrieve volume information for a given volume name.""" + url_params = {'shelf': 'cms', + 'orchStrRepo': '', + 'lv': volume_name} + reply = self.rpc('fetch', url_params, None) try: - response = self._esm_cmd(url) - info = response[0][1]['reply'][0] - return {"pool": info['lv']['containingPool'], - "repo": info['repoName'], - "vsxidx": info['lv']['lunIndex'], - "index": info['lv']['lvStatus']['exportedLun']['lun'], - "shelf": info['lv']['lvStatus']['exportedLun']['shelf']} - except Exception: - msg = _('Unable to retrive volume infos for volume %(volname)s') - raise CoraidESMException(msg % dict(volname=volume_name)) - - def _get_lun_address(self, volume_name): - """Return AoE Address for a given Volume.""" - volume_info = self._get_volume_info(volume_name) - shelf = volume_info['shelf'] - lun = volume_info['index'] - return {'shelf': shelf, 'lun': lun} - - def create_lun(self, volume_name, volume_size, repository): - """Create LUN on Coraid Backend Storage.""" - data = '[{"addr":"cms","data":"{' \ - '\\"servers\\":[\\"\\"],' \ - '\\"repoName\\":\\"%s\\",' \ - '\\"size\\":\\"%sG\\",' \ - '\\"lvName\\":\\"%s\\"}",' \ - '"op":"orchStrLun",' \ - '"args":"add"}]' % (repository, volume_size, - volume_name) - return self._configure(data) + volume_info = reply[0][1]['reply'][0] + except (IndexError, KeyError): + raise exception.VolumeNotFound(volume_id=volume_name) + return {'pool': volume_info['lv']['containingPool'], + 'repo': volume_info['repoName'], + 'lun': volume_info['lv']['lvStatus']['exportedLun']['lun'], + 'shelf': volume_info['lv']['lvStatus']['exportedLun']['shelf']} + + def get_volume_repository(self, volume_name): + volume_info = self.get_volume_info(volume_name) + return volume_info['repo'] + + def get_all_repos(self): + reply = self.rpc('fetch', {'orchStrRepo': ''}, None) + try: + return reply[0][1]['reply'] + except (IndexError, KeyError): + return [] - def delete_lun(self, volume_name): - """Delete LUN.""" + def ping(self): try: - volume_info = self._get_volume_info(volume_name) - repository = volume_info['repo'] - data = '[{"addr":"cms","data":"{' \ - '\\"repoName\\":\\"%(repo)s\\",' \ - '\\"lvName\\":\\"%(volname)s\\"}",' \ - '"op":"orchStrLun/verified",' \ - '"args":"delete"}]' % dict(repo=repository, - volname=volume_name) - return self._configure(data) - except Exception: - if self._check_esm_alive(): - return True - else: - return False + self.rpc('fetch', {}, None, allow_empty_response=True) + except Exception as e: + LOG.debug(_('Coraid Appliance ping failed: %s'), str(e)) + raise exception.CoraidESMNotAvailable(reason=str(e)) + + def create_lun(self, repository_name, volume_name, volume_size_in_gb): + request = {'addr': 'cms', + 'data': { + 'servers': [], + 'repoName': repository_name, + 'lvName': volume_name, + 'size': coraid_volume_size(volume_size_in_gb)}, + 'op': 'orchStrLun', + 'args': 'add'} + esm_result = self.esm_command(request) + LOG.debug(_('Volume "%(name)s" created with VSX LUN "%(lun)s"') % + {'name': volume_name, + 'lun': esm_result['firstParam']}) + return esm_result + + def delete_lun(self, volume_name): + repository_name = self.get_volume_repository(volume_name) + request = {'addr': 'cms', + 'data': { + 'repoName': repository_name, + 'lvName': volume_name}, + 'op': 'orchStrLun/verified', + 'args': 'delete'} + esm_result = self.esm_command(request) + LOG.debug(_('Volume "%s" deleted.'), volume_name) + return esm_result + + def resize_volume(self, volume_name, new_volume_size_in_gb): + LOG.debug(_('Resize volume "%(name)s" to %(size)s') % + {'name': volume_name, + 'size': new_volume_size_in_gb}) + repository = self.get_volume_repository(volume_name) + LOG.debug(_('Repository for volume "%(name)s" found: "%(repo)s"') % + {'name': volume_name, + 'repo': repository}) + + request = {'addr': 'cms', + 'data': { + 'lvName': volume_name, + 'newLvName': volume_name + '-resize', + 'size': coraid_volume_size(new_volume_size_in_gb), + 'repoName': repository}, + 'op': 'orchStrLunMods', + 'args': 'resize'} + esm_result = self.esm_command(request) + + LOG.debug(_('Volume "%(name)s" resized. New size is %(size)s') % + {'name': volume_name, + 'size': new_volume_size_in_gb}) + return esm_result def create_snapshot(self, volume_name, snapshot_name): - """Create Snapshot.""" - volume_info = self._get_volume_info(volume_name) - repository = volume_info['repo'] - data = '[{"addr":"cms","data":"{' \ - '\\"repoName\\":\\"%s\\",' \ - '\\"lvName\\":\\"%s\\",' \ - '\\"newLvName\\":\\"%s\\"}",' \ - '"op":"orchStrLunMods",' \ - '"args":"addClSnap"}]' % (repository, volume_name, - snapshot_name) - return self._configure(data) + volume_repository = self.get_volume_repository(volume_name) + request = {'addr': 'cms', + 'data': { + 'repoName': volume_repository, + 'lvName': volume_name, + 'newLvName': snapshot_name}, + 'op': 'orchStrLunMods', + 'args': 'addClSnap'} + esm_result = self.esm_command(request) + return esm_result def delete_snapshot(self, snapshot_name): - """Delete Snapshot.""" - snapshot_info = self._get_volume_info(snapshot_name) - repository = snapshot_info['repo'] - data = '[{"addr":"cms","data":"{' \ - '\\"repoName\\":\\"%s\\",' \ - '\\"lvName\\":\\"%s\\"}",' \ - '"op":"orchStrLunMods",' \ - '"args":"delClSnap"}]' % (repository, snapshot_name) - return self._configure(data) - - def create_volume_from_snapshot(self, snapshot_name, - volume_name, repository): - """Create a LUN from a Snapshot.""" - snapshot_info = self._get_volume_info(snapshot_name) - snapshot_repo = snapshot_info['repo'] - data = '[{"addr":"cms","data":"{' \ - '\\"lvName\\":\\"%s\\",' \ - '\\"repoName\\":\\"%s\\",' \ - '\\"newLvName\\":\\"%s\\",' \ - '\\"newRepoName\\":\\"%s\\"}",' \ - '"op":"orchStrLunMods",' \ - '"args":"addClone"}]' % (snapshot_name, snapshot_repo, - volume_name, repository) - return self._configure(data) - - def resize_volume(self, volume_name, volume_size): - volume_info = self._get_volume_info(volume_name) - repository = volume_info['repo'] - data = '[{"addr":"cms","data":"{' \ - '\\"lvName\\":\\"%s\\",' \ - '\\"newLvSize\\":\\"%s\\"}",' \ - '\\"repoName\\":\\"%s\\"}",' \ - '"op":"orchStrLunMods",' \ - '"args":"resizeVolume"}]' % (volume_name, - volume_size, - repository) - return self._configure(data) + repository_name = self.get_volume_repository(snapshot_name) + request = {'addr': 'cms', + 'data': { + 'repoName': repository_name, + 'lvName': snapshot_name}, + 'op': 'orchStrLunMods', + 'args': 'delClSnap'} + esm_result = self.esm_command(request) + return esm_result + + def create_volume_from_snapshot(self, + snapshot_name, + volume_name, + dest_repository_name): + snapshot_repo = self.get_volume_repository(snapshot_name) + request = {'addr': 'cms', + 'data': { + 'lvName': snapshot_name, + 'repoName': snapshot_repo, + 'newLvName': volume_name, + 'newRepoName': dest_repository_name}, + 'op': 'orchStrLunMods', + 'args': 'addClone'} + esm_result = self.esm_command(request) + return esm_result + + def clone_volume(self, + src_volume_name, + dst_volume_name, + dst_repository_name): + src_volume_info = self.get_volume_info(src_volume_name) + + if src_volume_info['repo'] != dst_repository_name: + raise exception.CoraidException( + _('Cannot create clone volume in different repository.')) + + request = {'addr': 'cms', + 'data': { + 'shelfLun': '{0}.{1}'.format(src_volume_info['shelf'], + src_volume_info['lun']), + 'lvName': src_volume_name, + 'repoName': src_volume_info['repo'], + 'newLvName': dst_volume_name, + 'newRepoName': dst_repository_name}, + 'op': 'orchStrLunMods', + 'args': 'addClone'} + return self.esm_command(request) class CoraidDriver(driver.VolumeDriver): """This is the Class to set in cinder.conf (volume_driver).""" + VERSION = '1.0.0' + def __init__(self, *args, **kwargs): super(CoraidDriver, self).__init__(*args, **kwargs) self.configuration.append_config_values(coraid_opts) + self._appliance = None + + self._stats = {'driver_version': self.VERSION, + 'free_capacity_gb': 'unknown', + 'reserved_percentage': 0, + 'storage_protocol': 'aoe', + 'total_capacity_gb': 'unknown', + 'vendor_name': 'Coraid'} + backend_name = self.configuration.safe_get('volume_backend_name') + self._stats['volume_backend_name'] = backend_name or 'EtherCloud ESM' def do_setup(self, context): """Initialize the volume driver.""" - self.esm = CoraidRESTClient(self.configuration.coraid_esm_address, - self.configuration.coraid_user, - self.configuration.coraid_group, - self.configuration.coraid_password) + esm_url = "https://{0}:8443".format( + self.configuration.coraid_esm_address) + + rest_client = CoraidRESTClient(esm_url) + self._appliance = CoraidAppliance(rest_client, + self.configuration.coraid_user, + self.configuration.coraid_password, + self.configuration.coraid_group) def check_for_setup_error(self): """Return an error if prerequisites aren't met.""" - if not self.esm._login(): - raise LookupError(_("Cannot login on Coraid ESM")) + self._appliance.ping() def _get_repository(self, volume_type): - """ - Return the ESM Repository from the Volume Type. + """Get the ESM Repository from the Volume Type. + The ESM Repository is stored into a volume_type_extra_specs key. """ volume_type_id = volume_type['id'] repository_key_name = self.configuration.coraid_repository_key repository = volume_types.get_volume_type_extra_specs( volume_type_id, repository_key_name) - return repository + # Remove keyword from repository name if needed + if repository.startswith(' '): + return repository[len(' '):] + else: + return repository def create_volume(self, volume): """Create a Volume.""" - try: - repository = self._get_repository(volume['volume_type']) - self.esm.create_lun(volume['name'], volume['size'], repository) - except Exception: - msg = _('Fail to create volume %(volname)s') - LOG.debug(msg % dict(volname=volume['name'])) - raise + repository = self._get_repository(volume['volume_type']) + self._appliance.create_lun(repository, volume['name'], volume['size']) # NOTE(jbr_): The manager currently interprets any return as # being the model_update for provider location. # return None to not break it (thank to jgriffith and DuncanT) return + def create_cloned_volume(self, volume, src_vref): + dst_volume_repository = self._get_repository(volume['volume_type']) + + self._appliance.clone_volume(src_vref['name'], + volume['name'], + dst_volume_repository) + + if volume['size'] != src_vref['size']: + self._appliance.resize_volume(volume['name'], volume['size']) + + return + def delete_volume(self, volume): """Delete a Volume.""" try: - self.esm.delete_lun(volume['name']) - except Exception: - msg = _('Failed to delete volume %(volname)s') - LOG.debug(msg % dict(volname=volume['name'])) - raise - return + self._appliance.delete_lun(volume['name']) + except exception.VolumeNotFound: + self._appliance.ping() def create_snapshot(self, snapshot): """Create a Snapshot.""" - volume_name = (self.configuration.volume_name_template - % snapshot['volume_id']) - snapshot_name = (self.configuration.snapshot_name_template - % snapshot['id']) - try: - self.esm.create_snapshot(volume_name, snapshot_name) - except Exception as e: - msg = _('Failed to Create Snapshot %(snapname)s') - LOG.debug(msg % dict(snapname=snapshot_name)) - raise - return + volume_name = snapshot['volume_name'] + snapshot_name = snapshot['name'] + self._appliance.create_snapshot(volume_name, snapshot_name) def delete_snapshot(self, snapshot): """Delete a Snapshot.""" - snapshot_name = (self.configuration.snapshot_name_template - % snapshot['id']) - try: - self.esm.delete_snapshot(snapshot_name) - except Exception: - msg = _('Failed to Delete Snapshot %(snapname)s') - LOG.debug(msg % dict(snapname=snapshot_name)) - raise - return + snapshot_name = snapshot['name'] + self._appliance.delete_snapshot(snapshot_name) def create_volume_from_snapshot(self, volume, snapshot): """Create a Volume from a Snapshot.""" - snapshot_name = (self.configuration.snapshot_name_template - % snapshot['id']) + snapshot_name = snapshot['name'] repository = self._get_repository(volume['volume_type']) - try: - self.esm.create_volume_from_snapshot(snapshot_name, - volume['name'], - repository) - resize = volume['size'] > snapshot['volume_size'] - if resize: - self.esm.resize_volume(volume['name'], volume['size']) - except Exception: - msg = _('Failed to Create Volume from Snapshot %(snapname)s') - LOG.debug(msg % dict(snapname=snapshot_name)) - raise - return + self._appliance.create_volume_from_snapshot(snapshot_name, + volume['name'], + repository) + if volume['size'] > snapshot['volume_size']: + self._appliance.resize_volume(volume['name'], volume['size']) def extend_volume(self, volume, new_size): - """Extend an Existing Volume.""" - try: - self.esm.resize_volume(volume['name'], new_size) - except Exception: - msg = _('Failed to Extend Volume %(volname)s') - LOG.debug(msg % dict(volname=volume['name'])) - raise + """Extend an existing volume.""" + self._appliance.resize_volume(volume['name'], new_size) return def initialize_connection(self, volume, connector): """Return connection information.""" - try: - infos = self.esm._get_lun_address(volume['name']) - shelf = infos['shelf'] - lun = infos['lun'] - - aoe_properties = { - 'target_shelf': shelf, - 'target_lun': lun, - } - return { - 'driver_volume_type': 'aoe', - 'data': aoe_properties, - } - except Exception: - msg = _('Failed to Initialize Connection. ' - 'Volume Name: %(volname)s ' - 'Shelf: %(shelf)s, ' - 'Lun: %(lun)s') - LOG.debug(msg % dict(volname=volume['name'], - shelf=shelf, - lun=lun)) - raise - return + volume_info = self._appliance.get_volume_info(volume['name']) + + shelf = volume_info['shelf'] + lun = volume_info['lun'] + + LOG.debug(_('Initialize connection %(shelf)s/%(lun)s for %(name)s') % + {'shelf': shelf, + 'lun': lun, + 'name': volume['name']}) + + aoe_properties = {'target_shelf': shelf, + 'target_lun': lun} + + return {'driver_volume_type': 'aoe', + 'data': aoe_properties} + + def _get_repository_capabilities(self): + repos_list = map(lambda i: i['profile']['fullName'] + ':' + i['name'], + self._appliance.get_all_repos()) + return ' '.join(repos_list) + + def update_volume_stats(self): + capabilities = self._get_repository_capabilities() + self._stats[self.configuration.coraid_repository_key] = capabilities def get_volume_stats(self, refresh=False): """Return Volume Stats.""" - data = {'driver_version': self.VERSION, - 'free_capacity_gb': 'unknown', - 'reserved_percentage': 0, - 'storage_protocol': 'aoe', - 'total_capacity_gb': 'unknown', - 'vendor_name': 'Coraid'} - backend_name = self.configuration.safe_get('volume_backend_name') - data['volume_backend_name'] = backend_name or 'EtherCloud ESM' - return data + if refresh: + self.update_volume_stats() + return self._stats def local_path(self, volume): pass @@ -457,6 +546,3 @@ class CoraidDriver(driver.VolumeDriver): def ensure_export(self, context, volume): pass - - def detach_volume(self, context, volume): - pass diff --git a/etc/cinder/rootwrap.d/volume.filters b/etc/cinder/rootwrap.d/volume.filters index b095475b9..0a8709969 100644 --- a/etc/cinder/rootwrap.d/volume.filters +++ b/etc/cinder/rootwrap.d/volume.filters @@ -77,3 +77,8 @@ mmlsconfig: CommandFilter, /usr/lpp/mmfs/bin/mmlsconfig, root mmlsfs: CommandFilter, /usr/lpp/mmfs/bin/mmlsfs, root find: CommandFilter, find, root mkfs: CommandFilter, mkfs, root + +# cinder/brick/initiator/connector.py: +aoe-revalidate: CommandFilter, aoe-revalidate, root +aoe-discover: CommandFilter, aoe-discover, root +aoe-flush: CommandFilter, aoe-flush, root -- 2.45.2