cinder/cinder/volume/drivers/coraid.py

422 lines
15 KiB
Python

# 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>
Contrib : Larry Matter <support@coraid.com>
"""
import cookielib
import os
import time
import urllib2
from oslo.config import cfg
from cinder import context
from cinder import exception
from cinder import flags
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
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_group',
default=False,
help='Group name of coraid_user (must have admin privilege)'),
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, 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 _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.
"""
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'."""
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
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)
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)
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)
self.configuration.append_config_values(coraid_opts)
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)
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 = self.configuration.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, e:
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