--- /dev/null
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2012 OpenStack LLC.
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from cinder import exception
+from cinder.openstack.common import log as logging
+from cinder import test
+from cinder.volume.drivers import coraid
+from cinder.volume.drivers.coraid import CoraidDriver
+from cinder.volume.drivers.coraid import CoraidRESTClient
+
+import cookielib
+import urllib2
+
+LOG = logging.getLogger(__name__)
+
+
+fake_esm_ipaddress = "192.168.0.1"
+fake_esm_username = "admin"
+fake_esm_password = "12345678"
+
+fake_volume_name = "volume-12345678-1234-1234-1234-1234567890ab"
+fake_volume_size = "10"
+fake_repository_name = "A-B:C:D"
+fake_pool_name = "FakePool"
+fake_aoetarget = 4081
+fake_shelf = 16
+fake_lun = 241
+
+fake_str_aoetarget = str(fake_aoetarget)
+fake_lun_addr = {"shelf": fake_shelf, "lun": fake_lun}
+
+fake_volume = {"name": fake_volume_name,
+ "size": fake_volume_size,
+ "volume_type": {"id": 1}}
+
+fake_volume_info = {"pool": fake_pool_name,
+ "repo": fake_repository_name,
+ "vsxidx": fake_aoetarget,
+ "index": fake_lun,
+ "shelf": fake_shelf}
+
+fake_lun_info = {"shelf": fake_shelf, "lun": fake_lun}
+
+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,
+ "volume_id": fake_volume_id}
+
+fake_configure_data = [{"addr": "cms", "data": "FAKE"}]
+
+fake_esm_fetch = [[
+ {"command": "super_fake_command_of_death"},
+ {"reply": [
+ {"lv":
+ {"containingPool": fake_pool_name,
+ "lunIndex": fake_aoetarget,
+ "name": fake_volume_name,
+ "lvStatus":
+ {"exportedLun":
+ {"lun": fake_lun,
+ "shelf": fake_shelf}}
+ },
+ "repoName": fake_repository_name}]}]]
+
+fake_esm_success = {"category": "provider",
+ "tracking": False,
+ "configState": "completedSuccessfully",
+ "heldPending": False,
+ "metaCROp": "noAction",
+ "message": None}
+
+
+class TestCoraidDriver(test.TestCase):
+ def setUp(self):
+ super(TestCoraidDriver, self).setUp()
+ self.esm_mock = self.mox.CreateMockAnything()
+ self.stubs.Set(coraid, 'CoraidRESTClient',
+ lambda *_, **__: self.esm_mock)
+ self.drv = CoraidDriver()
+ self.drv.do_setup({})
+
+ 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)
+
+ def test_delete_volume(self):
+ setattr(self.esm_mock, 'delete_lun',
+ lambda *_: True)
+ self.drv.delete_volume(fake_volume)
+
+ def test_initialize_connection(self):
+ setattr(self.esm_mock, '_get_lun_address',
+ lambda *_: fake_lun_addr)
+ self.drv.initialize_connection(fake_volume, '')
+
+ def test_create_snapshot(self):
+ setattr(self.esm_mock, 'create_snapshot',
+ lambda *_: True)
+ self.drv.create_snapshot(fake_snapshot)
+
+ def test_delete_snapshot(self):
+ setattr(self.esm_mock, 'delete_snapshot',
+ lambda *_: True)
+ self.drv.delete_snapshot(fake_snapshot)
+
+ def test_create_volume_from_snapshot(self):
+ setattr(self.esm_mock, 'create_volume_from_snapshot',
+ lambda *_: True)
+ self.stubs.Set(CoraidDriver, '_get_repository',
+ lambda *_: fake_repository_name)
+ self.drv.create_volume_from_snapshot(fake_volume, fake_snapshot)
+
+
+class TestCoraidRESTClient(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_password)
+
+ def test__configure(self):
+ setattr(self.rest_mock, '_configure',
+ lambda *_: True)
+ self.stubs.Set(CoraidRESTClient, '_esm',
+ lambda *_: fake_esm_success)
+ self.drv._configure(fake_configure_data)
+
+ def test__get_volume_info(self):
+ setattr(self.rest_mock, '_get_volume_info',
+ lambda *_: True)
+ self.stubs.Set(CoraidRESTClient, '_esm',
+ 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(self):
+ setattr(self.rest_mock, 'delete_lun',
+ lambda *_: True)
+ 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)
+ self.drv.delete_lun(fake_volume_name)
+
+ 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)
+
+ 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 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)
--- /dev/null
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+#
+# Copyright 2012 Alyseo.
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+"""
+Desc : Driver to store volumes on Coraid Appliances.
+Require : Coraid EtherCloud ESM, Coraid VSX and Coraid SRX.
+Author : Jean-Baptiste RANSY <openstack@alyseo.com>
+"""
+
+from cinder import context
+from cinder import exception
+from cinder import flags
+from cinder.openstack.common import cfg
+from cinder.openstack.common import jsonutils
+from cinder.openstack.common import log as logging
+from cinder.volume import driver
+from cinder.volume import volume_types
+
+import cookielib
+import os
+import time
+import urllib2
+
+
+LOG = logging.getLogger(__name__)
+
+FLAGS = flags.FLAGS
+coraid_opts = [
+ cfg.StrOpt('coraid_esm_address',
+ default='',
+ help='IP address of Coraid ESM'),
+ cfg.StrOpt('coraid_user',
+ default='admin',
+ help='User name to connect to Coraid ESM'),
+ cfg.StrOpt('coraid_password',
+ default='password',
+ help='Password to connect to Coraid ESM'),
+ cfg.StrOpt('coraid_repository_key',
+ default='coraid_repository',
+ help='Volume Type key name to store ESM Repository Name'),
+]
+FLAGS.register_opts(coraid_opts)
+
+
+class CoraidException(Exception):
+ def __init__(self, message=None, error=None):
+ super(CoraidException, self).__init__(message, error)
+
+ def __str__(self):
+ return '%s: %s' % self.args
+
+
+class CoraidRESTException(CoraidException):
+ pass
+
+
+class CoraidESMException(CoraidException):
+ pass
+
+
+class CoraidRESTClient(object):
+ """Executes volume driver commands on Coraid ESM EtherCloud Appliance."""
+
+ def __init__(self, ipaddress, user, password):
+ self.url = "https://%s:8443/" % ipaddress
+ self.user = user
+ 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'))
+ self._login()
+
+ 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._esm(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))
+ return True
+ else:
+ errmsg = response.get('message', '')
+ msg = _('Message : %(message)s')
+ raise CoraidESMException(msg % dict(message=errmsg))
+ return True
+
+ def _esm(self, url=False, data=None):
+ """
+ _esm 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.
+ """
+ if url:
+ url = self.url + url
+
+ req = urllib2.Request(url, data)
+
+ try:
+ res = self.urlOpener.open(req).read()
+ except Exception:
+ raise CoraidRESTException(_('ESM urlOpen error'))
+
+ try:
+ res_json = jsonutils.loads(res)
+ except Exception:
+ raise CoraidRESTException(_('JSON Error'))
+
+ return res_json
+ else:
+ raise CoraidRESTException(_('Request without URL'))
+
+ def _configure(self, data):
+ """In charge of all commands into 'configure'."""
+ self._login()
+ url = 'configure'
+ LOG.debug(_('Configure data : %s'), data)
+ response = self._esm(url, data)
+ LOG.debug(_("Configure response : %s"), response)
+ if response:
+ if response.get('configState') == 'completedSuccessfully':
+ return True
+ else:
+ errmsg = response.get('message', '')
+ msg = _('Message : %(message)s')
+ raise CoraidESMException(msg % dict(message=errmsg))
+ return False
+
+ def _get_volume_info(self, lvname):
+ """Fetch information for a given Volume or Snapshot."""
+ self._login()
+ url = 'fetch?shelf=cms&orchStrRepo&lv=%s' % (lvname)
+ response = self._esm(url)
+
+ items = []
+ for cmd, reply in response:
+ if len(reply['reply']) != 0:
+ items.append(reply['reply'])
+
+ volume_info = False
+ for item in items[0]:
+ if item['lv']['name'] == lvname:
+ volume_info = {
+ "pool": item['lv']['containingPool'],
+ "repo": item['repoName'],
+ "vsxidx": item['lv']['lunIndex'],
+ "index": item['lv']['lvStatus']['exportedLun']['lun'],
+ "shelf": item['lv']['lvStatus']['exportedLun']['shelf']}
+
+ if volume_info:
+ return volume_info
+ else:
+ msg = _('Informtion about Volume %(volname)s not found')
+ 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)
+
+ def delete_lun(self, volume_name):
+ """Delete LUN."""
+ volume_info = self._get_volume_info(volume_name)
+ repository = volume_info['repo']
+ data = '[{"addr":"cms","data":"{' \
+ '\\"repoName\\":\\"%s\\",' \
+ '\\"lvName\\":\\"%s\\"}",' \
+ '"op":"orchStrLun/verified",' \
+ '"args":"delete"}]' % (repository, volume_name)
+ return self._configure(data)
+
+ 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)
+
+ 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)
+
+
+class CoraidDriver(driver.VolumeDriver):
+ """This is the Class to set in cinder.conf (volume_driver)."""
+
+ def __init__(self, *args, **kwargs):
+ super(CoraidDriver, self).__init__(*args, **kwargs)
+
+ def do_setup(self, context):
+ """Initialize the volume driver."""
+ self.esm = CoraidRESTClient(FLAGS.coraid_esm_address,
+ FLAGS.coraid_user,
+ FLAGS.coraid_password)
+
+ 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"))
+
+ def _get_repository(self, volume_type):
+ """
+ Return 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 = FLAGS.coraid_repository_key
+ repository = volume_types.get_volume_type_extra_specs(
+ volume_type_id, repository_key_name)
+ 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
+ # 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 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
+
+ def create_snapshot(self, snapshot):
+ """Create a Snapshot."""
+ try:
+ volume_name = FLAGS.volume_name_template % snapshot['volume_id']
+ snapshot_name = FLAGS.snapshot_name_template % snapshot['id']
+ self.esm.create_snapshot(volume_name, snapshot_name)
+ except Exception:
+ msg = _('Failed to Create Snapshot %(snapname)s')
+ LOG.debug(msg % dict(snapname=snapshot_name))
+ raise
+ return
+
+ def delete_snapshot(self, snapshot):
+ """Delete a Snapshot."""
+ try:
+ snapshot_name = FLAGS.snapshot_name_template % snapshot['id']
+ self.esm.delete_snapshot(snapshot_name)
+ except Exception:
+ msg = _('Failed to Delete Snapshot %(snapname)s')
+ LOG.debug(msg % dict(snapname=snapshot_name))
+ raise
+ return
+
+ def create_volume_from_snapshot(self, volume, snapshot):
+ """Create a Volume from a Snapshot."""
+ try:
+ snapshot_name = FLAGS.snapshot_name_template % snapshot['id']
+ repository = self._get_repository(volume['volume_type'])
+ self.esm.create_volume_from_snapshot(snapshot_name,
+ volume['name'],
+ repository)
+ except Exception:
+ msg = _('Failed to Create Volume from Snapshot %(snapname)s')
+ LOG.debug(msg % dict(snapname=snapshot_name))
+ raise
+ 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
+
+ def get_volume_stats(self, refresh=False):
+ """Return Volume Stats."""
+ return {'driver_version': '1.0',
+ 'free_capacity_gb': 'unknown',
+ 'reserved_percentage': 0,
+ 'storage_protocol': 'aoe',
+ 'total_capacity_gb': 'unknown',
+ 'vendor_name': 'Coraid',
+ 'volume_backend_name': 'EtherCloud ESM'}
+
+ def local_path(self, volume):
+ pass
+
+ def create_export(self, context, volume):
+ pass
+
+ def remove_export(self, context, volume):
+ pass
+
+ def terminate_connection(self, volume, connector, **kwargs):
+ pass
+
+ def ensure_export(self, context, volume):
+ pass
+
+ def attach_volume(self, context, volume, instance_uuid, mountpoint):
+ pass
+
+ def detach_volume(self, context, volume):
+ pass