¡@

Home 

OpenStack Study: coraid.py

OpenStack Index

**** CubicPower OpenStack Study ****

# 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

Author : Alex Zasimov

Author : Nikolay Sobolevsky

Contrib : Larry Matter

"""

import cookielib

import math

import urllib

import urllib2

from oslo.config import cfg

import six.moves.urllib.parse as urlparse

from cinder import exception

from cinder.openstack.common import jsonutils

from cinder.openstack.common import lockutils

from cinder.openstack.common import log as logging

from cinder import units

from cinder.volume import driver

from cinder.volume import volume_types

LOG = logging.getLogger(__name__)

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='admin',

help='Name of group on Coraid ESM to which coraid_user belongs'

' (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'),

]

CONF = cfg.CONF

CONF.register_opts(coraid_opts)

ESM_SESSION_EXPIRED_STATES = ['GeneralAdminFailure',

'passwordInactivityTimeout',

'passwordAbsoluteTimeout']

**** CubicPower OpenStack Study ****

class CoraidRESTClient(object):

"""Executes REST RPC requests on Coraid ESM EtherCloud Appliance."""

**** CubicPower OpenStack Study ****

    def __init__(self, esm_url):

        self._check_esm_url(esm_url)

        self._esm_url = esm_url

        self._cookie_jar = cookielib.CookieJar()

        self._url_opener = urllib2.build_opener(

            urllib2.HTTPCookieProcessor(self._cookie_jar))

**** CubicPower OpenStack Study ****

    def _check_esm_url(self, esm_url):

        splitted = urlparse.urlsplit(esm_url)

        if splitted.scheme != 'https':

            raise ValueError(

                _('Invalid ESM url scheme "%s". Supported https only.') %

                splitted.scheme)

    @lockutils.synchronized('coraid_rpc', 'cinder-', False)

**** CubicPower OpenStack Study ****

    def rpc(self, handle, url_params, data, allow_empty_response=False):

        return self._rpc(handle, url_params, data, allow_empty_response)

**** CubicPower OpenStack Study ****

    def _rpc(self, handle, url_params, data, allow_empty_response):

        """Execute REST RPC using url /handle?url_params.

        Send JSON encoded data in body of POST request.

        Exceptions:

            urllib2.URLError

              1. Name or service not found (e.reason is socket.gaierror)

              2. Socket blocking operation timeout (e.reason is

                 socket.timeout)

              3. Network IO error (e.reason is socket.error)

            urllib2.HTTPError

              1. HTTP 404, HTTP 500 etc.

            CoraidJsonEncodeFailure - bad REST response

        """

        # Handle must be simple path, for example:

        #    /configure

        if '?' in handle or '&' in handle:

            raise ValueError(_('Invalid REST handle name. Expected path.'))

        # Request url includes base ESM url, handle path and optional

        # URL params.

        rest_url = urlparse.urljoin(self._esm_url, handle)

        encoded_url_params = urllib.urlencode(url_params)

        if encoded_url_params:

            rest_url += '?' + encoded_url_params

        if data is None:

            json_request = None

        else:

            json_request = jsonutils.dumps(data)

        request = urllib2.Request(rest_url, json_request)

        response = self._url_opener.open(request).read()

        try:

            if not response and allow_empty_response:

                reply = {}

            else:

                reply = jsonutils.loads(response)

        except (TypeError, ValueError) as exc:

            msg = (_('Call to json.loads() failed: %(ex)s.'

                     ' Response: %(resp)s') %

                   {'ex': exc, 'resp': response})

            raise exception.CoraidJsonEncodeFailure(msg)

        return reply

def to_coraid_kb(gb):

    return math.ceil(float(gb) * units.GiB / 1000)

def coraid_volume_size(gb):

    return '{0}K'.format(to_coraid_kb(gb))

**** CubicPower OpenStack Study ****

def to_coraid_kb(gb):

    return math.ceil(float(gb) * units.GiB / 1000)

**** CubicPower OpenStack Study ****

def coraid_volume_size(gb):

    return '{0}K'.format(to_coraid_kb(gb))

**** CubicPower OpenStack Study ****

class CoraidAppliance(object):

**** CubicPower OpenStack Study ****

    def __init__(self, rest_client, username, password, group):

        self._rest_client = rest_client

        self._username = username

        self._password = password

        self._group = group

        self._logined = False

**** CubicPower OpenStack Study ****

    def _login(self):

        """Login into ESM.

        Perform login request and return available groups.

        :returns: dict -- map with group_name to group_id

        """

        ADMIN_GROUP_PREFIX = 'admin group:'

        url_params = {'op': 'login',

                      'username': self._username,

                      'password': self._password}

        reply = self._rest_client.rpc('admin', url_params, 'Login')

        if reply['state'] != 'adminSucceed':

            raise exception.CoraidESMBadCredentials()

        # Read groups map from login reply.

        groups_map = {}

        for group_info in reply.get('values', []):

            full_group_name = group_info['fullPath']

            if full_group_name.startswith(ADMIN_GROUP_PREFIX):

                group_name = full_group_name[len(ADMIN_GROUP_PREFIX):]

                groups_map[group_name] = group_info['groupId']

        return groups_map

**** CubicPower OpenStack Study ****

    def _set_effective_group(self, groups_map, group):

        """Set effective group.

        Use groups_map returned from _login method.

        """

        try:

            group_id = groups_map[group]

        except KeyError:

            raise exception.CoraidESMBadGroup(group_name=group)

        url_params = {'op': 'setRbacGroup',

                      'groupId': group_id}

        reply = self._rest_client.rpc('admin', url_params, 'Group')

        if reply['state'] != 'adminSucceed':

            raise exception.CoraidESMBadCredentials()

        self._logined = True

**** CubicPower OpenStack Study ****

    def _ensure_session(self):

        if not self._logined:

            groups_map = self._login()

            self._set_effective_group(groups_map, self._group)

**** CubicPower OpenStack Study ****

    def _relogin(self):

        self._logined = False

        self._ensure_session()

**** CubicPower OpenStack Study ****

    def rpc(self, handle, url_params, data, allow_empty_response=False):

        self._ensure_session()

        relogin_attempts = 3

        # Do action, relogin if needed and repeat action.

        while True:

            reply = self._rest_client.rpc(handle, url_params, data,

                                          allow_empty_response)

            if self._is_session_expired(reply):

                relogin_attempts -= 1

                if relogin_attempts <= 0:

                    raise exception.CoraidESMReloginFailed()

                LOG.debug(_('Session is expired. Relogin on ESM.'))

                self._relogin()

            else:

                return reply

**** CubicPower OpenStack Study ****

    def _is_session_expired(self, reply):

        return ('state' in reply and

                reply['state'] in ESM_SESSION_EXPIRED_STATES and

                reply['metaCROp'] == 'reboot')

**** CubicPower OpenStack Study ****

    def _is_bad_config_state(self, reply):

        return (not reply or

                'configState' not in reply or

                reply['configState'] != 'completedSuccessfully')

**** CubicPower OpenStack Study ****

    def configure(self, json_request):

        reply = self.rpc('configure', {}, json_request)

        if self._is_bad_config_state(reply):

            # Calculate error message

            if not reply:

                reason = _('Reply is empty.')

            else:

                reason = reply.get('message', _('Error message is empty.'))

            raise exception.CoraidESMConfigureError(reason=reason)

        return reply

**** CubicPower OpenStack Study ****

    def esm_command(self, request):

        request['data'] = jsonutils.dumps(request['data'])

        return self.configure([request])

**** CubicPower OpenStack Study ****

    def get_volume_info(self, volume_name):

        """Retrieve volume information for a given volume name."""

        url_params = {'shelf': 'cms',

                      'orchStrRepo': '',

                      'lv': volume_name}

        reply = self.rpc('fetch', url_params, None)

        try:

            volume_info = reply[0][1]['reply'][0]

        except (IndexError, KeyError):

            raise exception.VolumeNotFound(volume_id=volume_name)

        return {'pool': volume_info['lv']['containingPool'],

                'repo': volume_info['repoName'],

                'lun': volume_info['lv']['lvStatus']['exportedLun']['lun'],

                'shelf': volume_info['lv']['lvStatus']['exportedLun']['shelf']}

**** CubicPower OpenStack Study ****

    def get_volume_repository(self, volume_name):

        volume_info = self.get_volume_info(volume_name)

        return volume_info['repo']

**** CubicPower OpenStack Study ****

    def get_all_repos(self):

        reply = self.rpc('fetch', {'orchStrRepo': ''}, None)

        try:

            return reply[0][1]['reply']

        except (IndexError, KeyError):

            return []

**** CubicPower OpenStack Study ****

    def ping(self):

        try:

            self.rpc('fetch', {}, None, allow_empty_response=True)

        except Exception as e:

            LOG.debug(_('Coraid Appliance ping failed: %s'), e)

            raise exception.CoraidESMNotAvailable(reason=e)

**** CubicPower OpenStack Study ****

    def create_lun(self, repository_name, volume_name, volume_size_in_gb):

        request = {'addr': 'cms',

                   'data': {

                       'servers': [],

                       'repoName': repository_name,

                       'lvName': volume_name,

                       'size': coraid_volume_size(volume_size_in_gb)},

                   'op': 'orchStrLun',

                   'args': 'add'}

        esm_result = self.esm_command(request)

        LOG.debug(_('Volume "%(name)s" created with VSX LUN "%(lun)s"') %

                  {'name': volume_name,

                   'lun': esm_result['firstParam']})

        return esm_result

**** CubicPower OpenStack Study ****

    def delete_lun(self, volume_name):

        repository_name = self.get_volume_repository(volume_name)

        request = {'addr': 'cms',

                   'data': {

                       'repoName': repository_name,

                       'lvName': volume_name},

                   'op': 'orchStrLun/verified',

                   'args': 'delete'}

        esm_result = self.esm_command(request)

        LOG.debug(_('Volume "%s" deleted.'), volume_name)

        return esm_result

**** CubicPower OpenStack Study ****

    def resize_volume(self, volume_name, new_volume_size_in_gb):

        LOG.debug(_('Resize volume "%(name)s" to %(size)s GB.') %

                  {'name': volume_name,

                   'size': new_volume_size_in_gb})

        repository = self.get_volume_repository(volume_name)

        LOG.debug(_('Repository for volume "%(name)s" found: "%(repo)s"') %

                  {'name': volume_name,

                   'repo': repository})

        request = {'addr': 'cms',

                   'data': {

                       'lvName': volume_name,

                       'newLvName': volume_name + '-resize',

                       'size': coraid_volume_size(new_volume_size_in_gb),

                       'repoName': repository},

                   'op': 'orchStrLunMods',

                   'args': 'resize'}

        esm_result = self.esm_command(request)

        LOG.debug(_('Volume "%(name)s" resized. New size is %(size)s GB.') %

                  {'name': volume_name,

                   'size': new_volume_size_in_gb})

        return esm_result

**** CubicPower OpenStack Study ****

    def create_snapshot(self, volume_name, snapshot_name):

        volume_repository = self.get_volume_repository(volume_name)

        request = {'addr': 'cms',

                   'data': {

                       'repoName': volume_repository,

                       'lvName': volume_name,

                       'newLvName': snapshot_name},

                   'op': 'orchStrLunMods',

                   'args': 'addClSnap'}

        esm_result = self.esm_command(request)

        return esm_result

**** CubicPower OpenStack Study ****

    def delete_snapshot(self, snapshot_name):

        repository_name = self.get_volume_repository(snapshot_name)

        request = {'addr': 'cms',

                   'data': {

                       'repoName': repository_name,

                       'lvName': snapshot_name},

                   'op': 'orchStrLunMods',

                   'args': 'delClSnap'}

        esm_result = self.esm_command(request)

        return esm_result

**** CubicPower OpenStack Study ****

    def create_volume_from_snapshot(self,

                                    snapshot_name,

                                    volume_name,

                                    dest_repository_name):

        snapshot_repo = self.get_volume_repository(snapshot_name)

        request = {'addr': 'cms',

                   'data': {

                       'lvName': snapshot_name,

                       'repoName': snapshot_repo,

                       'newLvName': volume_name,

                       'newRepoName': dest_repository_name},

                   'op': 'orchStrLunMods',

                   'args': 'addClone'}

        esm_result = self.esm_command(request)

        return esm_result

**** CubicPower OpenStack Study ****

    def clone_volume(self,

                     src_volume_name,

                     dst_volume_name,

                     dst_repository_name):

        src_volume_info = self.get_volume_info(src_volume_name)

        if src_volume_info['repo'] != dst_repository_name:

            raise exception.CoraidException(

                _('Cannot create clone volume in different repository.'))

        request = {'addr': 'cms',

                   'data': {

                       'shelfLun': '{0}.{1}'.format(src_volume_info['shelf'],

                                                    src_volume_info['lun']),

                       'lvName': src_volume_name,

                       'repoName': src_volume_info['repo'],

                       'newLvName': dst_volume_name,

                       'newRepoName': dst_repository_name},

                   'op': 'orchStrLunMods',

                   'args': 'addClone'}

        return self.esm_command(request)

**** CubicPower OpenStack Study ****

class CoraidDriver(driver.VolumeDriver):

"""This is the Class to set in cinder.conf (volume_driver)."""

VERSION = '1.0.0'

**** CubicPower OpenStack Study ****

    def __init__(self, *args, **kwargs):

        super(CoraidDriver, self).__init__(*args, **kwargs)

        self.configuration.append_config_values(coraid_opts)

        self._stats = {'driver_version': self.VERSION,

                       'free_capacity_gb': 'unknown',

                       'reserved_percentage': 0,

                       'storage_protocol': 'aoe',

                       'total_capacity_gb': 'unknown',

                       'vendor_name': 'Coraid'}

        backend_name = self.configuration.safe_get('volume_backend_name')

        self._stats['volume_backend_name'] = backend_name or 'EtherCloud ESM'

    @property

**** CubicPower OpenStack Study ****

    def appliance(self):

        # NOTE(nsobolevsky): This is workaround for bug in the ESM appliance.

        # If there is a lot of request with the same session/cookie/connection,

        # the appliance could corrupt all following request in session.

        # For that purpose we just create a new appliance.

        esm_url = "https://{0}:8443".format(

            self.configuration.coraid_esm_address)

        return CoraidAppliance(CoraidRESTClient(esm_url),

                               self.configuration.coraid_user,

                               self.configuration.coraid_password,

                               self.configuration.coraid_group)

**** CubicPower OpenStack Study ****

    def check_for_setup_error(self):

        """Return an error if prerequisites aren't met."""

        self.appliance.ping()

**** CubicPower OpenStack Study ****

    def _get_repository(self, volume_type):

        """Get 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)

        # Remove  keyword from repository name if needed

        if repository.startswith(' '):

            return repository[len(' '):]

        else:

            return repository

**** CubicPower OpenStack Study ****

    def create_volume(self, volume):

        """Create a Volume."""

        repository = self._get_repository(volume['volume_type'])

        self.appliance.create_lun(repository, volume['name'], volume['size'])

**** CubicPower OpenStack Study ****

    def create_cloned_volume(self, volume, src_vref):

        dst_volume_repository = self._get_repository(volume['volume_type'])

        self.appliance.clone_volume(src_vref['name'],

                                    volume['name'],

                                    dst_volume_repository)

        if volume['size'] != src_vref['size']:

            self.appliance.resize_volume(volume['name'], volume['size'])

**** CubicPower OpenStack Study ****

    def delete_volume(self, volume):

        """Delete a Volume."""

        try:

            self.appliance.delete_lun(volume['name'])

        except exception.VolumeNotFound:

            self.appliance.ping()

**** CubicPower OpenStack Study ****

    def create_snapshot(self, snapshot):

        """Create a Snapshot."""

        volume_name = snapshot['volume_name']

        snapshot_name = snapshot['name']

        self.appliance.create_snapshot(volume_name, snapshot_name)

**** CubicPower OpenStack Study ****

    def delete_snapshot(self, snapshot):

        """Delete a Snapshot."""

        snapshot_name = snapshot['name']

        self.appliance.delete_snapshot(snapshot_name)

**** CubicPower OpenStack Study ****

    def create_volume_from_snapshot(self, volume, snapshot):

        """Create a Volume from a Snapshot."""

        snapshot_name = snapshot['name']

        repository = self._get_repository(volume['volume_type'])

        self.appliance.create_volume_from_snapshot(snapshot_name,

                                                   volume['name'],

                                                   repository)

        if volume['size'] > snapshot['volume_size']:

            self.appliance.resize_volume(volume['name'], volume['size'])

**** CubicPower OpenStack Study ****

    def extend_volume(self, volume, new_size):

        """Extend an existing volume."""

        self.appliance.resize_volume(volume['name'], new_size)

**** CubicPower OpenStack Study ****

    def initialize_connection(self, volume, connector):

        """Return connection information."""

        volume_info = self.appliance.get_volume_info(volume['name'])

        shelf = volume_info['shelf']

        lun = volume_info['lun']

        LOG.debug(_('Initialize connection %(shelf)s/%(lun)s for %(name)s') %

                  {'shelf': shelf,

                   'lun': lun,

                   'name': volume['name']})

        aoe_properties = {'target_shelf': shelf,

                          'target_lun': lun}

        return {'driver_volume_type': 'aoe',

                'data': aoe_properties}

**** CubicPower OpenStack Study ****

    def _get_repository_capabilities(self):

        repos_list = map(lambda i: i['profile']['fullName'] + ':' + i['name'],

                         self.appliance.get_all_repos())

        return ' '.join(repos_list)

**** CubicPower OpenStack Study ****

    def update_volume_stats(self):

        capabilities = self._get_repository_capabilities()

        self._stats[self.configuration.coraid_repository_key] = capabilities

**** CubicPower OpenStack Study ****

    def get_volume_stats(self, refresh=False):

        """Return Volume Stats."""

        if refresh:

            self.update_volume_stats()

        return self._stats

**** CubicPower OpenStack Study ****

    def local_path(self, volume):

        pass

**** CubicPower OpenStack Study ****

    def create_export(self, context, volume):

        pass

**** CubicPower OpenStack Study ****

    def remove_export(self, context, volume):

        pass

**** CubicPower OpenStack Study ****

    def terminate_connection(self, volume, connector, **kwargs):

        pass

**** CubicPower OpenStack Study ****

    def ensure_export(self, context, volume):

        pass