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
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") %
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})
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.')
import string
import time
+import mox
+
from cinder.brick import exception
from cinder.brick.initiator import connector
from cinder.brick.initiator import host_driver
self.assertTrue(obj.__class__.__name__,
"FibreChannelConnector")
+ obj = connector.InitiatorConnector.factory('aoe')
+ self.assertTrue(obj.__class__.__name__,
+ "AoEConnector")
+
self.assertRaises(ValueError,
connector.InitiatorConnector.factory,
"bogus")
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, {})
# 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
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,
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,
},
"repoName": fake_repository_name}]}]]
+fake_esm_fetch_no_volume = [[
+ {"command": "super_fake_command"},
+ {"reply": []}]]
+
fake_esm_success = {"category": "provider",
"tracking": False,
"configState": "completedSuccessfully",
"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
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('<in> {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()
Desc : Driver to store volumes on Coraid Appliances.
Require : Coraid EtherCloud ESM, Coraid VSX and Coraid SRX.
Author : Jean-Baptiste RANSY <openstack@alyseo.com>
+Author : Alex Zasimov <azasimov@mirantis.com>
+Author : Nikolay Sobolevsky <nsobolevsky@mirantis.com>
Contrib : Larry Matter <support@coraid.com>
"""
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
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 <esm_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 <in> keyword from repository name if needed
+ if repository.startswith('<in> '):
+ return repository[len('<in> '):]
+ 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
def ensure_export(self, context, volume):
pass
-
- def detach_volume(self, context, volume):
- pass
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