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
This commit is contained in:
Jean-Baptiste RANSY 2013-02-08 21:10:39 +01:00
parent 835fb61442
commit 695e3a848a
3 changed files with 615 additions and 0 deletions

214
cinder/tests/test_coraid.py Normal file
View File

@ -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)

View File

@ -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

View 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