From 695e3a848ac35a45d5765776641d75782fa1c234 Mon Sep 17 00:00:00 2001 From: Jean-Baptiste RANSY Date: Fri, 8 Feb 2013 21:10:39 +0100 Subject: [PATCH] Adding support for Coraid AoE SANs Appliances. 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 | 214 ++++++++++++++++++ cinder/volume/drivers/coraid.py | 388 ++++++++++++++++++++++++++++++++ cinder/volume/volume_types.py | 13 ++ 3 files changed, 615 insertions(+) create mode 100644 cinder/tests/test_coraid.py create mode 100644 cinder/volume/drivers/coraid.py diff --git a/cinder/tests/test_coraid.py b/cinder/tests/test_coraid.py new file mode 100644 index 000000000..175725d16 --- /dev/null +++ b/cinder/tests/test_coraid.py @@ -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 index 000000000..442dacf83 --- /dev/null +++ b/cinder/volume/drivers/coraid.py @@ -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 +""" + +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 diff --git a/cinder/volume/volume_types.py b/cinder/volume/volume_types.py index 82c513b9c..dab962685 100644 --- a/cinder/volume/volume_types.py +++ b/cinder/volume/volume_types.py @@ -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 -- 2.45.2