]> review.fuel-infra Code Review - openstack-build/cinder-build.git/commitdiff
Adding support for Coraid AoE SANs Appliances.
authorJean-Baptiste RANSY <jean-baptiste.ransy@alyseo.com>
Fri, 8 Feb 2013 20:10:39 +0000 (21:10 +0100)
committerJean-Baptiste RANSY <jean-baptiste.ransy@alyseo.com>
Fri, 15 Feb 2013 20:13:35 +0000 (21:13 +0100)
This driver provide support for Coraid hardware storage appliances
using AoE (ATA Over Ethernet) protocol.

Implements blueprint coraid-volume-driver

Reference to Nova patch libvirt-aoe :
https://review.openstack.org/21101

The following operations are supported :
-- Volume Creation with Volume Types
-- Volume Deletion
-- Volume Attach
-- Volume Detach
-- Snapshot Creation
-- Snapshot Deletion
-- Create Volume from Snapshot
-- Volume Stats

The driver only work when operating on EtherCloud ESM,
Coraid VSX and Coraid SRX Appliances.

Change-Id: I7c8dde0c99698b52c151a4db0fb1bb94d516db61

cinder/tests/test_coraid.py [new file with mode: 0644]
cinder/volume/drivers/coraid.py [new file with mode: 0644]
cinder/volume/volume_types.py

diff --git a/cinder/tests/test_coraid.py b/cinder/tests/test_coraid.py
new file mode 100644 (file)
index 0000000..175725d
--- /dev/null
@@ -0,0 +1,214 @@
+# 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)
diff --git a/cinder/volume/drivers/coraid.py b/cinder/volume/drivers/coraid.py
new file mode 100644 (file)
index 0000000..442dacf
--- /dev/null
@@ -0,0 +1,388 @@
+# 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
index 82c513b9c44f25414192342765ca78c9cfca4d8a..dab9626857ff351a1f5eb46f187265ef814bd5ac 100644 (file)
@@ -143,3 +143,16 @@ def is_key_value_present(volume_type_id, key, value, volume_type=None):
         return False
     else:
         return True
+
+
+def get_volume_type_extra_specs(volume_type_id, key=False):
+    volume_type = get_volume_type(context.get_admin_context(),
+                                  volume_type_id)
+    extra_specs = volume_type['extra_specs']
+    if key:
+        if extra_specs.get(key):
+            return extra_specs.get(key)
+        else:
+            return False
+    else:
+        return extra_specs