Update cinderlib
Sync the cinderlib repository with patches proposed to the Cinder repository: - https://review.openstack.org/620669 - https://review.openstack.org/620670 - https://review.openstack.org/620671
This commit is contained in:
committed by
Gorka Eguileor
parent
c491c71f47
commit
85776225cb
4
Makefile
4
Makefile
@@ -52,13 +52,13 @@ python-requirements:
|
|||||||
pip install -e .
|
pip install -e .
|
||||||
|
|
||||||
lint: python-requirements ## check style with flake8
|
lint: python-requirements ## check style with flake8
|
||||||
flake8 cinderlib tests
|
flake8 cinderlib
|
||||||
|
|
||||||
unit-tests:
|
unit-tests:
|
||||||
tox -epy27
|
tox -epy27
|
||||||
|
|
||||||
functional-tests:
|
functional-tests:
|
||||||
unit2 discover -v -s tests/functional
|
CL_FTEST_CFG=`pwd`/tools/lvm.yaml unit2 discover -v -s cinderlib/tests/functional
|
||||||
|
|
||||||
test-all: ## run tests on every Python version with tox
|
test-all: ## run tests on every Python version with tox
|
||||||
tox
|
tox
|
||||||
|
|||||||
@@ -1,12 +1,26 @@
|
|||||||
|
# Copyright (c) 2018, Red Hat, Inc.
|
||||||
|
# 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 __future__ import absolute_import
|
from __future__ import absolute_import
|
||||||
from cinderlib import workarounds # noqa
|
import pkg_resources
|
||||||
from cinderlib import cinderlib
|
|
||||||
from cinderlib import serialization
|
|
||||||
from cinderlib import objects
|
|
||||||
|
|
||||||
__author__ = """Gorka Eguileor"""
|
from cinderlib import cinderlib
|
||||||
__email__ = 'geguileo@redhat.com'
|
from cinderlib import objects
|
||||||
__version__ = '0.2.2'
|
from cinderlib import serialization
|
||||||
|
from cinderlib import workarounds # noqa
|
||||||
|
|
||||||
|
__version__ = pkg_resources.get_distribution('cinder').version
|
||||||
|
|
||||||
DEFAULT_PROJECT_ID = objects.DEFAULT_PROJECT_ID
|
DEFAULT_PROJECT_ID = objects.DEFAULT_PROJECT_ID
|
||||||
DEFAULT_USER_ID = objects.DEFAULT_USER_ID
|
DEFAULT_USER_ID = objects.DEFAULT_USER_ID
|
||||||
@@ -23,3 +37,5 @@ dumps = serialization.dumps
|
|||||||
|
|
||||||
setup = cinderlib.setup
|
setup = cinderlib.setup
|
||||||
Backend = cinderlib.Backend
|
Backend = cinderlib.Backend
|
||||||
|
|
||||||
|
get_connector_properties = objects.brick_connector.get_connector_properties
|
||||||
|
|||||||
@@ -17,19 +17,26 @@ from __future__ import absolute_import
|
|||||||
import json as json_lib
|
import json as json_lib
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
import six
|
||||||
|
|
||||||
from cinder import coordination
|
from cinder import coordination
|
||||||
# NOTE(geguileo): If we want to prevent eventlet from monkey_patching we would
|
from cinder.db import api as db_api
|
||||||
# need to do something about volume's L27-32.
|
from cinder import objects as cinder_objects
|
||||||
# NOTE(geguileo): Probably a good idea not to depend on cinder.cmd.volume
|
|
||||||
# having all the other imports as they could change.
|
# We need this here until we remove from cinder/volume/manager.py:
|
||||||
from cinder.cmd import volume as volume_cmd
|
# VA_LIST = objects.VolumeAttachmentList
|
||||||
|
cinder_objects.register_all() # noqa
|
||||||
|
|
||||||
from cinder import utils
|
from cinder import utils
|
||||||
from cinder.volume import configuration
|
from cinder.volume import configuration
|
||||||
import nos_brick
|
from cinder.volume import manager
|
||||||
|
from oslo_config import cfg
|
||||||
|
from oslo_log import log as oslo_logging
|
||||||
from oslo_utils import importutils
|
from oslo_utils import importutils
|
||||||
import urllib3
|
import urllib3
|
||||||
|
|
||||||
|
import cinderlib
|
||||||
|
from cinderlib import nos_brick
|
||||||
from cinderlib import objects
|
from cinderlib import objects
|
||||||
from cinderlib import persistence
|
from cinderlib import persistence
|
||||||
from cinderlib import serialization
|
from cinderlib import serialization
|
||||||
@@ -54,6 +61,9 @@ class Backend(object):
|
|||||||
"""
|
"""
|
||||||
backends = {}
|
backends = {}
|
||||||
global_initialization = False
|
global_initialization = False
|
||||||
|
# Some drivers try access the DB directly for extra specs on creation.
|
||||||
|
# With this dictionary the DB class can get the necessary data
|
||||||
|
_volumes_inflight = {}
|
||||||
|
|
||||||
def __init__(self, volume_backend_name, **driver_cfg):
|
def __init__(self, volume_backend_name, **driver_cfg):
|
||||||
if not self.global_initialization:
|
if not self.global_initialization:
|
||||||
@@ -61,13 +71,13 @@ class Backend(object):
|
|||||||
driver_cfg['volume_backend_name'] = volume_backend_name
|
driver_cfg['volume_backend_name'] = volume_backend_name
|
||||||
Backend.backends[volume_backend_name] = self
|
Backend.backends[volume_backend_name] = self
|
||||||
|
|
||||||
conf = self._get_config(**driver_cfg)
|
conf = self._set_backend_config(driver_cfg)
|
||||||
self.driver = importutils.import_object(
|
self.driver = importutils.import_object(
|
||||||
conf.volume_driver,
|
conf.volume_driver,
|
||||||
configuration=conf,
|
configuration=conf,
|
||||||
db=self.persistence.db,
|
db=self.persistence.db,
|
||||||
host='%s@%s' % (objects.CONFIGURED_HOST, volume_backend_name),
|
host='%s@%s' % (cfg.CONF.host, volume_backend_name),
|
||||||
cluster_name=None, # No clusters for now: volume_cmd.CONF.cluster,
|
cluster_name=None, # We don't user cfg.CONF.cluster for now
|
||||||
active_backend_id=None) # No failover for now
|
active_backend_id=None) # No failover for now
|
||||||
self.driver.do_setup(objects.CONTEXT)
|
self.driver.do_setup(objects.CONTEXT)
|
||||||
self.driver.check_for_setup_error()
|
self.driver.check_for_setup_error()
|
||||||
@@ -79,7 +89,7 @@ class Backend(object):
|
|||||||
# init_capabilities already calls get_volume_stats with refresh=True
|
# init_capabilities already calls get_volume_stats with refresh=True
|
||||||
# so we can call it without refresh to get pool names.
|
# so we can call it without refresh to get pool names.
|
||||||
self._pool_names = tuple(pool['pool_name']
|
self._pool_names = tuple(pool['pool_name']
|
||||||
for pool in self.get_volume_stats()['pools'])
|
for pool in self.stats()['pools'])
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def pool_names(self):
|
def pool_names(self):
|
||||||
@@ -107,8 +117,18 @@ class Backend(object):
|
|||||||
volume_name=volume_name)
|
volume_name=volume_name)
|
||||||
|
|
||||||
def stats(self, refresh=False):
|
def stats(self, refresh=False):
|
||||||
stats = self.driver.get_volume_stats(refresh=refresh)
|
stats_data = self.driver.get_volume_stats(refresh=refresh)
|
||||||
return stats
|
# Fill pools for legacy driver reports
|
||||||
|
if stats_data and 'pools' not in stats_data:
|
||||||
|
pool = stats_data.copy()
|
||||||
|
pool['pool_name'] = self.id
|
||||||
|
for key in ('driver_version', 'shared_targets',
|
||||||
|
'sparse_copy_volume', 'storage_protocol',
|
||||||
|
'vendor_name', 'volume_backend_name'):
|
||||||
|
pool.pop(key, None)
|
||||||
|
stats_data['pools'] = [pool]
|
||||||
|
|
||||||
|
return stats_data
|
||||||
|
|
||||||
def create_volume(self, size, name='', description='', bootable=False,
|
def create_volume(self, size, name='', description='', bootable=False,
|
||||||
**kwargs):
|
**kwargs):
|
||||||
@@ -125,9 +145,14 @@ class Backend(object):
|
|||||||
del self._volumes[i]
|
del self._volumes[i]
|
||||||
break
|
break
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _start_creating_volume(cls, volume):
|
||||||
|
cls._volumes_inflight[volume.id] = volume
|
||||||
|
|
||||||
def _volume_created(self, volume):
|
def _volume_created(self, volume):
|
||||||
if self._volumes is not None:
|
if self._volumes is not None:
|
||||||
self._volumes.append(volume)
|
self._volumes.append(volume)
|
||||||
|
self._volumes_inflight.pop(volume.id, None)
|
||||||
|
|
||||||
def validate_connector(self, connector_dict):
|
def validate_connector(self, connector_dict):
|
||||||
"""Raise exception if missing info for volume's connect call."""
|
"""Raise exception if missing info for volume's connect call."""
|
||||||
@@ -144,12 +169,83 @@ class Backend(object):
|
|||||||
for backend in cls.backends.values():
|
for backend in cls.backends.values():
|
||||||
backend.driver.db = cls.persistence.db
|
backend.driver.db = cls.persistence.db
|
||||||
|
|
||||||
|
# Replace the standard DB implementation instance with the one from
|
||||||
|
# the persistence plugin.
|
||||||
|
db_api.IMPL = cls.persistence.db
|
||||||
|
|
||||||
|
# NOTE(geguileo): Staticmethod used instead of classmethod to make it work
|
||||||
|
# on Python3 when assigning the unbound method.
|
||||||
|
@staticmethod
|
||||||
|
def _config_parse(self):
|
||||||
|
"""Replacer oslo_config.cfg.ConfigParser.parse for in-memory cfg."""
|
||||||
|
res = super(cfg.ConfigParser, self).parse(Backend._config_string_io)
|
||||||
|
return res
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _update_cinder_config(cls):
|
||||||
|
"""Parse in-memory file to update OSLO configuration used by Cinder."""
|
||||||
|
cls._config_string_io.seek(0)
|
||||||
|
cls._parser.write(cls._config_string_io)
|
||||||
|
cls._config_string_io.seek(0)
|
||||||
|
cfg.CONF.reload_config_files()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _set_cinder_config(cls, host, locks_path, cinder_config_params):
|
||||||
|
"""Setup the parser with all the known Cinder configuration."""
|
||||||
|
cfg.CONF.set_default('state_path', os.getcwd())
|
||||||
|
cfg.CONF.set_default('lock_path', '$state_path', 'oslo_concurrency')
|
||||||
|
|
||||||
|
cls._parser = six.moves.configparser.SafeConfigParser()
|
||||||
|
cls._parser.set('DEFAULT', 'enabled_backends', '')
|
||||||
|
|
||||||
|
if locks_path:
|
||||||
|
cls._parser.add_section('oslo_concurrency')
|
||||||
|
cls._parser.set('oslo_concurrency', 'lock_path', locks_path)
|
||||||
|
cls._parser.add_section('coordination')
|
||||||
|
cls._parser.set('coordination',
|
||||||
|
'backend_url',
|
||||||
|
'file://' + locks_path)
|
||||||
|
if host:
|
||||||
|
cls._parser.set('DEFAULT', 'host', host)
|
||||||
|
|
||||||
|
# All other configuration options go into the DEFAULT section
|
||||||
|
for key, value in cinder_config_params.items():
|
||||||
|
if not isinstance(value, six.string_types):
|
||||||
|
value = six.text_type(value)
|
||||||
|
cls._parser.set('DEFAULT', key, value)
|
||||||
|
|
||||||
|
# We replace the OSLO's default parser to read from a StringIO instead
|
||||||
|
# of reading from a file.
|
||||||
|
cls._config_string_io = six.moves.StringIO()
|
||||||
|
cfg.ConfigParser.parse = six.create_unbound_method(cls._config_parse,
|
||||||
|
cfg.ConfigParser)
|
||||||
|
|
||||||
|
# Update the configuration with the options we have configured
|
||||||
|
cfg.CONF(project='cinder', version=cinderlib.__version__,
|
||||||
|
default_config_files=['in_memory_file'])
|
||||||
|
cls._update_cinder_config()
|
||||||
|
|
||||||
|
def _set_backend_config(self, driver_cfg):
|
||||||
|
backend_name = driver_cfg['volume_backend_name']
|
||||||
|
self._parser.add_section(backend_name)
|
||||||
|
for key, value in driver_cfg.items():
|
||||||
|
if not isinstance(value, six.string_types):
|
||||||
|
value = six.text_type(value)
|
||||||
|
self._parser.set(backend_name, key, value)
|
||||||
|
self._parser.set('DEFAULT', 'enabled_backends',
|
||||||
|
','.join(self.backends.keys()))
|
||||||
|
self._update_cinder_config()
|
||||||
|
config = configuration.Configuration(manager.volume_backend_opts,
|
||||||
|
config_group=backend_name)
|
||||||
|
return config
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def global_setup(cls, file_locks_path=None, root_helper='sudo',
|
def global_setup(cls, file_locks_path=None, root_helper='sudo',
|
||||||
suppress_requests_ssl_warnings=True, disable_logs=True,
|
suppress_requests_ssl_warnings=True, disable_logs=True,
|
||||||
non_uuid_ids=False, output_all_backend_info=False,
|
non_uuid_ids=False, output_all_backend_info=False,
|
||||||
project_id=None, user_id=None, persistence_config=None,
|
project_id=None, user_id=None, persistence_config=None,
|
||||||
fail_on_missing_backend=True, host=None, **log_params):
|
fail_on_missing_backend=True, host=None,
|
||||||
|
**cinder_config_params):
|
||||||
# Global setup can only be set once
|
# Global setup can only be set once
|
||||||
if cls.global_initialization:
|
if cls.global_initialization:
|
||||||
raise Exception('Already setup')
|
raise Exception('Already setup')
|
||||||
@@ -159,20 +255,15 @@ class Backend(object):
|
|||||||
cls.project_id = project_id
|
cls.project_id = project_id
|
||||||
cls.user_id = user_id
|
cls.user_id = user_id
|
||||||
cls.non_uuid_ids = non_uuid_ids
|
cls.non_uuid_ids = non_uuid_ids
|
||||||
objects.CONFIGURED_HOST = host or volume_cmd.CONF.host
|
|
||||||
|
|
||||||
cls.set_persistence(persistence_config)
|
cls.set_persistence(persistence_config)
|
||||||
|
cls._set_cinder_config(host, file_locks_path, cinder_config_params)
|
||||||
volume_cmd.CONF.version = volume_cmd.version.version_string()
|
|
||||||
volume_cmd.CONF.register_opt(
|
|
||||||
configuration.cfg.StrOpt('stateless_cinder'),
|
|
||||||
group=configuration.SHARED_CONF_GROUP)
|
|
||||||
|
|
||||||
serialization.setup(cls)
|
serialization.setup(cls)
|
||||||
|
|
||||||
cls._set_logging(disable_logs, **log_params)
|
cls._set_logging(disable_logs)
|
||||||
cls._set_priv_helper(root_helper)
|
cls._set_priv_helper(root_helper)
|
||||||
cls._set_coordinator(file_locks_path)
|
coordination.COORDINATOR.start()
|
||||||
|
|
||||||
if suppress_requests_ssl_warnings:
|
if suppress_requests_ssl_warnings:
|
||||||
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
||||||
@@ -182,43 +273,21 @@ class Backend(object):
|
|||||||
cls.global_initialization = True
|
cls.global_initialization = True
|
||||||
cls.output_all_backend_info = output_all_backend_info
|
cls.output_all_backend_info = output_all_backend_info
|
||||||
|
|
||||||
def _get_config(self, volume_backend_name, **kwargs):
|
|
||||||
volume_cmd.CONF.register_opt(volume_cmd.host_opt,
|
|
||||||
group=volume_backend_name)
|
|
||||||
backend_opts = getattr(volume_cmd.CONF, volume_backend_name)
|
|
||||||
for key, value in kwargs.items():
|
|
||||||
setattr(backend_opts, key, value)
|
|
||||||
config = configuration.Configuration([],
|
|
||||||
config_group=volume_backend_name)
|
|
||||||
return config
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _set_logging(cls, disable_logs, **log_params):
|
def _set_logging(cls, disable_logs):
|
||||||
if disable_logs:
|
if disable_logs:
|
||||||
logging.Logger.disabled = property(lambda s: True,
|
logging.Logger.disabled = property(lambda s: True,
|
||||||
lambda s, x: None)
|
lambda s, x: None)
|
||||||
return
|
return
|
||||||
|
|
||||||
for key, value in log_params.items():
|
oslo_logging.setup(cfg.CONF, 'cinder')
|
||||||
volume_cmd.CONF.set_override(key, value)
|
logging.captureWarnings(True)
|
||||||
volume_cmd.logging.setup(volume_cmd.CONF, 'cinder')
|
|
||||||
volume_cmd.python_logging.captureWarnings(True)
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _set_priv_helper(cls, root_helper):
|
def _set_priv_helper(cls, root_helper):
|
||||||
utils.get_root_helper = lambda: root_helper
|
utils.get_root_helper = lambda: root_helper
|
||||||
nos_brick.init(root_helper)
|
nos_brick.init(root_helper)
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def _set_coordinator(cls, file_locks_path):
|
|
||||||
file_locks_path = file_locks_path or os.getcwd()
|
|
||||||
volume_cmd.CONF.set_override('lock_path', file_locks_path,
|
|
||||||
'oslo_concurrency')
|
|
||||||
volume_cmd.CONF.set_override('backend_url',
|
|
||||||
'file://' + file_locks_path,
|
|
||||||
'coordination')
|
|
||||||
coordination.COORDINATOR.start()
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def config(self):
|
def config(self):
|
||||||
if self.output_all_backend_info:
|
if self.output_all_backend_info:
|
||||||
|
|||||||
@@ -20,10 +20,18 @@ NotFound = exception.NotFound
|
|||||||
VolumeNotFound = exception.VolumeNotFound
|
VolumeNotFound = exception.VolumeNotFound
|
||||||
SnapshotNotFound = exception.SnapshotNotFound
|
SnapshotNotFound = exception.SnapshotNotFound
|
||||||
ConnectionNotFound = exception.VolumeAttachmentNotFound
|
ConnectionNotFound = exception.VolumeAttachmentNotFound
|
||||||
|
InvalidVolume = exception.InvalidVolume
|
||||||
|
|
||||||
|
|
||||||
class InvalidPersistence(Exception):
|
class InvalidPersistence(Exception):
|
||||||
__msg = 'Invalid persistence storage: %s'
|
__msg = 'Invalid persistence storage: %s.'
|
||||||
|
|
||||||
def __init__(self, name):
|
def __init__(self, name):
|
||||||
super(InvalidPersistence, self).__init__(self.__msg % name)
|
super(InvalidPersistence, self).__init__(self.__msg % name)
|
||||||
|
|
||||||
|
|
||||||
|
class NotLocal(Exception):
|
||||||
|
__msg = "Volume %s doesn't seem to be attached locally."
|
||||||
|
|
||||||
|
def __init__(self, name):
|
||||||
|
super(NotLocal, self).__init__(self.__msg % name)
|
||||||
|
|||||||
164
cinderlib/nos_brick.py
Normal file
164
cinderlib/nos_brick.py
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
# Copyright (c) 2018, Red Hat, Inc.
|
||||||
|
# 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.
|
||||||
|
"""Helper code to attach/detach out of OpenStack
|
||||||
|
|
||||||
|
OS-Brick is meant to be used within OpenStack, which means that there are some
|
||||||
|
issues when using it on non OpenStack systems.
|
||||||
|
|
||||||
|
Here we take care of:
|
||||||
|
|
||||||
|
- Making sure we can work without privsep and using sudo directly
|
||||||
|
- Replacing an unlink privsep method that would run python code privileged
|
||||||
|
- Local attachment of RBD volumes using librados
|
||||||
|
|
||||||
|
Some of these changes may be later moved to OS-Brick. For now we just copied it
|
||||||
|
from the nos-brick repository.
|
||||||
|
"""
|
||||||
|
import functools
|
||||||
|
import os
|
||||||
|
|
||||||
|
from os_brick import exception
|
||||||
|
from os_brick.initiator import connector
|
||||||
|
from os_brick.initiator import connectors
|
||||||
|
from os_brick.privileged import rootwrap
|
||||||
|
from oslo_concurrency import processutils as putils
|
||||||
|
from oslo_privsep import priv_context
|
||||||
|
from oslo_utils import fileutils
|
||||||
|
|
||||||
|
|
||||||
|
class RBDConnector(connectors.rbd.RBDConnector):
|
||||||
|
""""Connector class to attach/detach RBD volumes locally.
|
||||||
|
|
||||||
|
OS-Brick's implementation covers only 2 cases:
|
||||||
|
|
||||||
|
- Local attachment on controller node.
|
||||||
|
- Returning a file object on non controller nodes.
|
||||||
|
|
||||||
|
We need a third one, local attachment on non controller node.
|
||||||
|
"""
|
||||||
|
def connect_volume(self, connection_properties):
|
||||||
|
# NOTE(e0ne): sanity check if ceph-common is installed.
|
||||||
|
try:
|
||||||
|
self._execute('which', 'rbd')
|
||||||
|
except putils.ProcessExecutionError:
|
||||||
|
msg = 'ceph-common package not installed'
|
||||||
|
raise exception.BrickException(msg)
|
||||||
|
|
||||||
|
# Extract connection parameters and generate config file
|
||||||
|
try:
|
||||||
|
user = connection_properties['auth_username']
|
||||||
|
pool, volume = connection_properties['name'].split('/')
|
||||||
|
cluster_name = connection_properties.get('cluster_name')
|
||||||
|
monitor_ips = connection_properties.get('hosts')
|
||||||
|
monitor_ports = connection_properties.get('ports')
|
||||||
|
keyring = connection_properties.get('keyring')
|
||||||
|
except IndexError:
|
||||||
|
msg = 'Malformed connection properties'
|
||||||
|
raise exception.BrickException(msg)
|
||||||
|
|
||||||
|
conf = self._create_ceph_conf(monitor_ips, monitor_ports,
|
||||||
|
str(cluster_name), user,
|
||||||
|
keyring)
|
||||||
|
|
||||||
|
# Map RBD volume if it's not already mapped
|
||||||
|
rbd_dev_path = self.get_rbd_device_name(pool, volume)
|
||||||
|
if (not os.path.islink(rbd_dev_path) or
|
||||||
|
not os.path.exists(os.path.realpath(rbd_dev_path))):
|
||||||
|
cmd = ['rbd', 'map', volume, '--pool', pool, '--conf', conf]
|
||||||
|
cmd += self._get_rbd_args(connection_properties)
|
||||||
|
self._execute(*cmd, root_helper=self._root_helper,
|
||||||
|
run_as_root=True)
|
||||||
|
|
||||||
|
return {'path': os.path.realpath(rbd_dev_path),
|
||||||
|
'conf': conf,
|
||||||
|
'type': 'block'}
|
||||||
|
|
||||||
|
def check_valid_device(self, path, run_as_root=True):
|
||||||
|
"""Verify an existing RBD handle is connected and valid."""
|
||||||
|
try:
|
||||||
|
self._execute('dd', 'if=' + path, 'of=/dev/null', 'bs=4096',
|
||||||
|
'count=1', root_helper=self._root_helper,
|
||||||
|
run_as_root=True)
|
||||||
|
except putils.ProcessExecutionError:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def disconnect_volume(self, connection_properties, device_info,
|
||||||
|
force=False, ignore_errors=False):
|
||||||
|
|
||||||
|
pool, volume = connection_properties['name'].split('/')
|
||||||
|
conf_file = device_info['conf']
|
||||||
|
dev_name = self.get_rbd_device_name(pool, volume)
|
||||||
|
cmd = ['rbd', 'unmap', dev_name, '--conf', conf_file]
|
||||||
|
cmd += self._get_rbd_args(connection_properties)
|
||||||
|
self._execute(*cmd, root_helper=self._root_helper,
|
||||||
|
run_as_root=True)
|
||||||
|
fileutils.delete_if_exists(conf_file)
|
||||||
|
|
||||||
|
|
||||||
|
ROOT_HELPER = 'sudo'
|
||||||
|
|
||||||
|
|
||||||
|
def unlink_root(*links, **kwargs):
|
||||||
|
no_errors = kwargs.get('no_errors', False)
|
||||||
|
raise_at_end = kwargs.get('raise_at_end', False)
|
||||||
|
exc = exception.ExceptionChainer()
|
||||||
|
catch_exception = no_errors or raise_at_end
|
||||||
|
for link in links:
|
||||||
|
with exc.context(catch_exception, 'Unlink failed for %s', link):
|
||||||
|
putils.execute('unlink', link, run_as_root=True,
|
||||||
|
root_helper=ROOT_HELPER)
|
||||||
|
if not no_errors and raise_at_end and exc:
|
||||||
|
raise exc
|
||||||
|
|
||||||
|
|
||||||
|
def init(root_helper='sudo'):
|
||||||
|
global ROOT_HELPER
|
||||||
|
ROOT_HELPER = root_helper
|
||||||
|
priv_context.init(root_helper=[root_helper])
|
||||||
|
|
||||||
|
existing_bgcp = connector.get_connector_properties
|
||||||
|
existing_bcp = connector.InitiatorConnector.factory
|
||||||
|
|
||||||
|
def my_bgcp(*args, **kwargs):
|
||||||
|
if len(args):
|
||||||
|
args = list(args)
|
||||||
|
args[0] = ROOT_HELPER
|
||||||
|
else:
|
||||||
|
kwargs['root_helper'] = ROOT_HELPER
|
||||||
|
kwargs['execute'] = rootwrap.custom_execute
|
||||||
|
return existing_bgcp(*args, **kwargs)
|
||||||
|
|
||||||
|
def my_bgc(protocol, *args, **kwargs):
|
||||||
|
if len(args):
|
||||||
|
# args is a tuple and we cannot do assignments
|
||||||
|
args = list(args)
|
||||||
|
args[0] = ROOT_HELPER
|
||||||
|
else:
|
||||||
|
kwargs['root_helper'] = ROOT_HELPER
|
||||||
|
kwargs['execute'] = rootwrap.custom_execute
|
||||||
|
|
||||||
|
# OS-Brick's implementation for RBD is not good enough for us
|
||||||
|
if protocol == 'rbd':
|
||||||
|
factory = RBDConnector
|
||||||
|
else:
|
||||||
|
factory = functools.partial(existing_bcp, protocol)
|
||||||
|
|
||||||
|
return factory(*args, **kwargs)
|
||||||
|
|
||||||
|
connector.get_connector_properties = my_bgcp
|
||||||
|
connector.InitiatorConnector.factory = staticmethod(my_bgc)
|
||||||
|
if hasattr(rootwrap, 'unlink_root'):
|
||||||
|
rootwrap.unlink_root = unlink_root
|
||||||
@@ -15,18 +15,17 @@
|
|||||||
|
|
||||||
from __future__ import absolute_import
|
from __future__ import absolute_import
|
||||||
import json as json_lib
|
import json as json_lib
|
||||||
|
import sys
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
from cinder import context
|
from cinder import context
|
||||||
# NOTE(geguileo): Probably a good idea not to depend on cinder.cmd.volume
|
|
||||||
# having all the other imports as they could change.
|
|
||||||
from cinder.cmd import volume as volume_cmd
|
|
||||||
from cinder import objects as cinder_objs
|
|
||||||
from cinder import exception as cinder_exception
|
from cinder import exception as cinder_exception
|
||||||
|
from cinder import objects as cinder_objs
|
||||||
from cinder.objects import base as cinder_base_ovo
|
from cinder.objects import base as cinder_base_ovo
|
||||||
from os_brick import exception as brick_exception
|
from os_brick import exception as brick_exception
|
||||||
from os_brick import initiator as brick_initiator
|
from os_brick import initiator as brick_initiator
|
||||||
from os_brick.initiator import connector as brick_connector
|
from os_brick.initiator import connector as brick_connector
|
||||||
|
from oslo_config import cfg
|
||||||
from oslo_log import log as logging
|
from oslo_log import log as logging
|
||||||
from oslo_utils import timeutils
|
from oslo_utils import timeutils
|
||||||
import six
|
import six
|
||||||
@@ -40,11 +39,9 @@ DEFAULT_USER_ID = 'cinderlib'
|
|||||||
BACKEND_NAME_SNAPSHOT_FIELD = 'progress'
|
BACKEND_NAME_SNAPSHOT_FIELD = 'progress'
|
||||||
CONNECTIONS_OVO_FIELD = 'volume_attachment'
|
CONNECTIONS_OVO_FIELD = 'volume_attachment'
|
||||||
|
|
||||||
CONFIGURED_HOST = 'cinderlib'
|
|
||||||
|
|
||||||
# This cannot go in the setup method because cinderlib objects need them to
|
# This cannot go in the setup method because cinderlib objects need them to
|
||||||
# be setup to set OVO_CLASS
|
# be setup to set OVO_CLASS
|
||||||
volume_cmd.objects.register_all()
|
cinder_objs.register_all()
|
||||||
|
|
||||||
|
|
||||||
class KeyValue(object):
|
class KeyValue(object):
|
||||||
@@ -58,6 +55,7 @@ class KeyValue(object):
|
|||||||
|
|
||||||
class Object(object):
|
class Object(object):
|
||||||
"""Base class for our resource representation objects."""
|
"""Base class for our resource representation objects."""
|
||||||
|
SIMPLE_JSON_IGNORE = tuple()
|
||||||
DEFAULT_FIELDS_VALUES = {}
|
DEFAULT_FIELDS_VALUES = {}
|
||||||
LAZY_PROPERTIES = tuple()
|
LAZY_PROPERTIES = tuple()
|
||||||
backend_class = None
|
backend_class = None
|
||||||
@@ -107,7 +105,7 @@ class Object(object):
|
|||||||
# Configure OVOs to support non_uuid_ids
|
# Configure OVOs to support non_uuid_ids
|
||||||
if non_uuid_ids:
|
if non_uuid_ids:
|
||||||
for ovo_name in cinder_base_ovo.CinderObjectRegistry.obj_classes():
|
for ovo_name in cinder_base_ovo.CinderObjectRegistry.obj_classes():
|
||||||
ovo_cls = getattr(volume_cmd.objects, ovo_name)
|
ovo_cls = getattr(cinder_objs, ovo_name)
|
||||||
if 'id' in ovo_cls.fields:
|
if 'id' in ovo_cls.fields:
|
||||||
ovo_cls.fields['id'] = cinder_base_ovo.fields.StringField()
|
ovo_cls.fields['id'] = cinder_base_ovo.fields.StringField()
|
||||||
|
|
||||||
@@ -142,14 +140,28 @@ class Object(object):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def json(self):
|
def json(self):
|
||||||
ovo = self._ovo.obj_to_primitive()
|
return self.to_json(simplified=False)
|
||||||
|
|
||||||
|
def to_json(self, simplified=True):
|
||||||
|
visited = set()
|
||||||
|
if simplified:
|
||||||
|
for field in self.SIMPLE_JSON_IGNORE:
|
||||||
|
if self._ovo.obj_attr_is_set(field):
|
||||||
|
visited.add(id(getattr(self._ovo, field)))
|
||||||
|
ovo = self._ovo.obj_to_primitive(visited=visited)
|
||||||
return {'class': type(self).__name__,
|
return {'class': type(self).__name__,
|
||||||
'backend': getattr(self.backend, 'config', self.backend),
|
# If no driver loaded, just return the name of the backend
|
||||||
|
'backend': getattr(self.backend, 'config',
|
||||||
|
{'volume_backend_name': self.backend}),
|
||||||
'ovo': ovo}
|
'ovo': ovo}
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def jsons(self):
|
def jsons(self):
|
||||||
return json_lib.dumps(self.json, separators=(',', ':'))
|
return self.to_jsons(simplified=False)
|
||||||
|
|
||||||
|
def to_jsons(self, simplified=True):
|
||||||
|
json_data = self.to_json(simplified)
|
||||||
|
return json_lib.dumps(json_data, separators=(',', ':'))
|
||||||
|
|
||||||
def _only_ovo_data(self, ovo):
|
def _only_ovo_data(self, ovo):
|
||||||
if isinstance(ovo, dict):
|
if isinstance(ovo, dict):
|
||||||
@@ -204,6 +216,11 @@ class Object(object):
|
|||||||
raise AttributeError('Attribute _ovo is not yet set')
|
raise AttributeError('Attribute _ovo is not yet set')
|
||||||
return getattr(self._ovo, name)
|
return getattr(self._ovo, name)
|
||||||
|
|
||||||
|
def _raise_with_resource(self):
|
||||||
|
exc_info = sys.exc_info()
|
||||||
|
exc_info[1].resource = self
|
||||||
|
six.reraise(*exc_info)
|
||||||
|
|
||||||
|
|
||||||
class NamedObject(Object):
|
class NamedObject(Object):
|
||||||
def __init__(self, backend, **fields_data):
|
def __init__(self, backend, **fields_data):
|
||||||
@@ -228,6 +245,7 @@ class NamedObject(Object):
|
|||||||
|
|
||||||
class LazyVolumeAttr(object):
|
class LazyVolumeAttr(object):
|
||||||
LAZY_PROPERTIES = ('volume',)
|
LAZY_PROPERTIES = ('volume',)
|
||||||
|
_volume = None
|
||||||
|
|
||||||
def __init__(self, volume):
|
def __init__(self, volume):
|
||||||
if volume:
|
if volume:
|
||||||
@@ -260,7 +278,8 @@ class LazyVolumeAttr(object):
|
|||||||
|
|
||||||
|
|
||||||
class Volume(NamedObject):
|
class Volume(NamedObject):
|
||||||
OVO_CLASS = volume_cmd.objects.Volume
|
OVO_CLASS = cinder_objs.Volume
|
||||||
|
SIMPLE_JSON_IGNORE = ('snapshots', 'volume_attachment')
|
||||||
DEFAULT_FIELDS_VALUES = {
|
DEFAULT_FIELDS_VALUES = {
|
||||||
'size': 1,
|
'size': 1,
|
||||||
'user_id': Object.CONTEXT.user_id,
|
'user_id': Object.CONTEXT.user_id,
|
||||||
@@ -273,7 +292,7 @@ class Volume(NamedObject):
|
|||||||
}
|
}
|
||||||
LAZY_PROPERTIES = ('snapshots', 'connections')
|
LAZY_PROPERTIES = ('snapshots', 'connections')
|
||||||
|
|
||||||
_ignore_keys = ('id', CONNECTIONS_OVO_FIELD, 'snapshots')
|
_ignore_keys = ('id', CONNECTIONS_OVO_FIELD, 'snapshots', 'volume_type')
|
||||||
|
|
||||||
def __init__(self, backend_or_vol, pool_name=None, **kwargs):
|
def __init__(self, backend_or_vol, pool_name=None, **kwargs):
|
||||||
# Accept backend name for convenience
|
# Accept backend name for convenience
|
||||||
@@ -283,19 +302,27 @@ class Volume(NamedObject):
|
|||||||
elif isinstance(backend_or_vol, self.backend_class):
|
elif isinstance(backend_or_vol, self.backend_class):
|
||||||
backend_name = backend_or_vol.id
|
backend_name = backend_or_vol.id
|
||||||
elif isinstance(backend_or_vol, Volume):
|
elif isinstance(backend_or_vol, Volume):
|
||||||
backend_name, pool = backend_or_vol._ovo.host.split('#')
|
backend_str, pool = backend_or_vol._ovo.host.split('#')
|
||||||
|
backend_name = backend_str.split('@')[-1]
|
||||||
pool_name = pool_name or pool
|
pool_name = pool_name or pool
|
||||||
for key in backend_or_vol._ovo.fields:
|
for key in backend_or_vol._ovo.fields:
|
||||||
if (backend_or_vol._ovo.obj_attr_is_set(key) and
|
if (backend_or_vol._ovo.obj_attr_is_set(key) and
|
||||||
key not in self._ignore_keys):
|
key not in self._ignore_keys):
|
||||||
kwargs.setdefault(key, getattr(backend_or_vol._ovo, key))
|
kwargs.setdefault(key, getattr(backend_or_vol._ovo, key))
|
||||||
|
if backend_or_vol.volume_type:
|
||||||
|
kwargs.setdefault('extra_specs',
|
||||||
|
backend_or_vol.volume_type.extra_specs)
|
||||||
|
if backend_or_vol.volume_type.qos_specs:
|
||||||
|
kwargs.setdefault(
|
||||||
|
'qos_specs',
|
||||||
|
backend_or_vol.volume_type.qos_specs.specs)
|
||||||
backend_or_vol = backend_or_vol.backend
|
backend_or_vol = backend_or_vol.backend
|
||||||
|
|
||||||
if '__ovo' not in kwargs:
|
if '__ovo' not in kwargs:
|
||||||
kwargs[CONNECTIONS_OVO_FIELD] = (
|
kwargs[CONNECTIONS_OVO_FIELD] = (
|
||||||
volume_cmd.objects.VolumeAttachmentList(context=self.CONTEXT))
|
cinder_objs.VolumeAttachmentList(context=self.CONTEXT))
|
||||||
kwargs['snapshots'] = (
|
kwargs['snapshots'] = (
|
||||||
volume_cmd.objects.SnapshotList(context=self.CONTEXT))
|
cinder_objs.SnapshotList(context=self.CONTEXT))
|
||||||
self._snapshots = []
|
self._snapshots = []
|
||||||
self._connections = []
|
self._connections = []
|
||||||
|
|
||||||
@@ -309,9 +336,10 @@ class Volume(NamedObject):
|
|||||||
# If we overwrote the host, then we ignore pool_name and don't set a
|
# If we overwrote the host, then we ignore pool_name and don't set a
|
||||||
# default value or copy the one from the source either.
|
# default value or copy the one from the source either.
|
||||||
if 'host' not in kwargs and '__ovo' not in kwargs:
|
if 'host' not in kwargs and '__ovo' not in kwargs:
|
||||||
|
# TODO(geguileo): Add pool support
|
||||||
pool_name = pool_name or backend_or_vol.pool_names[0]
|
pool_name = pool_name or backend_or_vol.pool_names[0]
|
||||||
self._ovo.host = ('%s@%s#%s' %
|
self._ovo.host = ('%s@%s#%s' %
|
||||||
(CONFIGURED_HOST, backend_name, pool_name))
|
(cfg.CONF.host, backend_name, pool_name))
|
||||||
|
|
||||||
if qos_specs or extra_specs:
|
if qos_specs or extra_specs:
|
||||||
if qos_specs:
|
if qos_specs:
|
||||||
@@ -406,6 +434,7 @@ class Volume(NamedObject):
|
|||||||
return vol
|
return vol
|
||||||
|
|
||||||
def create(self):
|
def create(self):
|
||||||
|
self.backend._start_creating_volume(self)
|
||||||
try:
|
try:
|
||||||
model_update = self.backend.driver.create_volume(self._ovo)
|
model_update = self.backend.driver.create_volume(self._ovo)
|
||||||
self._ovo.status = 'available'
|
self._ovo.status = 'available'
|
||||||
@@ -414,22 +443,23 @@ class Volume(NamedObject):
|
|||||||
self.backend._volume_created(self)
|
self.backend._volume_created(self)
|
||||||
except Exception:
|
except Exception:
|
||||||
self._ovo.status = 'error'
|
self._ovo.status = 'error'
|
||||||
# TODO: raise with the vol info
|
self._raise_with_resource()
|
||||||
raise
|
|
||||||
finally:
|
finally:
|
||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
def delete(self):
|
def delete(self):
|
||||||
# Some backends delete existing snapshots while others leave them
|
if self.snapshots:
|
||||||
|
msg = 'Cannot delete volume %s with snapshots' % self.id
|
||||||
|
raise exception.InvalidVolume(reason=msg)
|
||||||
try:
|
try:
|
||||||
self.backend.driver.delete_volume(self._ovo)
|
self.backend.driver.delete_volume(self._ovo)
|
||||||
self.persistence.delete_volume(self)
|
self.persistence.delete_volume(self)
|
||||||
self.backend._volume_removed(self)
|
self.backend._volume_removed(self)
|
||||||
|
self.status = 'deleted'
|
||||||
except Exception:
|
except Exception:
|
||||||
# We don't change status to error on deletion error, we assume it
|
self.status = 'error_deleting'
|
||||||
# just didn't complete.
|
self.save()
|
||||||
# TODO: raise with the vol info
|
self._raise_with_resource()
|
||||||
raise
|
|
||||||
|
|
||||||
def extend(self, size):
|
def extend(self, size):
|
||||||
volume = self._ovo
|
volume = self._ovo
|
||||||
@@ -442,14 +472,14 @@ class Volume(NamedObject):
|
|||||||
volume.previous_status = None
|
volume.previous_status = None
|
||||||
except Exception:
|
except Exception:
|
||||||
volume.status = 'error'
|
volume.status = 'error'
|
||||||
# TODO: raise with the vol info
|
self._raise_with_resource()
|
||||||
raise
|
|
||||||
finally:
|
finally:
|
||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
def clone(self, **new_vol_attrs):
|
def clone(self, **new_vol_attrs):
|
||||||
new_vol_attrs['source_vol_id'] = self.id
|
new_vol_attrs['source_vol_id'] = self.id
|
||||||
new_vol = Volume(self, **new_vol_attrs)
|
new_vol = Volume(self, **new_vol_attrs)
|
||||||
|
self.backend._start_creating_volume(new_vol)
|
||||||
try:
|
try:
|
||||||
model_update = self.backend.driver.create_cloned_volume(
|
model_update = self.backend.driver.create_cloned_volume(
|
||||||
new_vol._ovo, self._ovo)
|
new_vol._ovo, self._ovo)
|
||||||
@@ -459,15 +489,16 @@ class Volume(NamedObject):
|
|||||||
self.backend._volume_created(new_vol)
|
self.backend._volume_created(new_vol)
|
||||||
except Exception:
|
except Exception:
|
||||||
new_vol.status = 'error'
|
new_vol.status = 'error'
|
||||||
# TODO: raise with the new volume info
|
new_vol._raise_with_resource()
|
||||||
raise
|
|
||||||
finally:
|
finally:
|
||||||
new_vol.save()
|
new_vol.save()
|
||||||
return new_vol
|
return new_vol
|
||||||
|
|
||||||
def create_snapshot(self, name='', description='', **kwargs):
|
def create_snapshot(self, name='', description='', **kwargs):
|
||||||
snap = Snapshot(self, name=name, description=description, **kwargs)
|
snap = Snapshot(self, name=name, description=description, **kwargs)
|
||||||
|
try:
|
||||||
snap.create()
|
snap.create()
|
||||||
|
finally:
|
||||||
if self._snapshots is not None:
|
if self._snapshots is not None:
|
||||||
self._snapshots.append(snap)
|
self._snapshots.append(snap)
|
||||||
self._ovo.snapshots.objects.append(snap._ovo)
|
self._ovo.snapshots.objects.append(snap._ovo)
|
||||||
@@ -476,7 +507,7 @@ class Volume(NamedObject):
|
|||||||
def attach(self):
|
def attach(self):
|
||||||
connector_dict = brick_connector.get_connector_properties(
|
connector_dict = brick_connector.get_connector_properties(
|
||||||
self.backend_class.root_helper,
|
self.backend_class.root_helper,
|
||||||
volume_cmd.CONF.my_ip,
|
cfg.CONF.my_ip,
|
||||||
self.backend.configuration.use_multipath_for_image_xfer,
|
self.backend.configuration.use_multipath_for_image_xfer,
|
||||||
self.backend.configuration.enforce_multipath_for_image_xfer)
|
self.backend.configuration.enforce_multipath_for_image_xfer)
|
||||||
conn = self.connect(connector_dict)
|
conn = self.connect(connector_dict)
|
||||||
@@ -489,7 +520,7 @@ class Volume(NamedObject):
|
|||||||
|
|
||||||
def detach(self, force=False, ignore_errors=False):
|
def detach(self, force=False, ignore_errors=False):
|
||||||
if not self.local_attach:
|
if not self.local_attach:
|
||||||
raise Exception('Not attached')
|
raise exception.NotLocal(self.id)
|
||||||
exc = brick_exception.ExceptionChainer()
|
exc = brick_exception.ExceptionChainer()
|
||||||
|
|
||||||
conn = self.local_attach
|
conn = self.local_attach
|
||||||
@@ -523,13 +554,12 @@ class Volume(NamedObject):
|
|||||||
self.save()
|
self.save()
|
||||||
except Exception:
|
except Exception:
|
||||||
self._remove_export()
|
self._remove_export()
|
||||||
# TODO: Improve raised exception
|
self._raise_with_resource()
|
||||||
raise
|
|
||||||
return conn
|
return conn
|
||||||
|
|
||||||
def _disconnect(self, connection):
|
def _disconnect(self, connection):
|
||||||
self._remove_export()
|
self._remove_export()
|
||||||
if self._connections is not None:
|
if self._connections:
|
||||||
self._connections.remove(connection)
|
self._connections.remove(connection)
|
||||||
ovo_conns = getattr(self._ovo, CONNECTIONS_OVO_FIELD).objects
|
ovo_conns = getattr(self._ovo, CONNECTIONS_OVO_FIELD).objects
|
||||||
ovo_conns.remove(connection._ovo)
|
ovo_conns.remove(connection._ovo)
|
||||||
@@ -543,7 +573,7 @@ class Volume(NamedObject):
|
|||||||
self._disconnect(connection)
|
self._disconnect(connection)
|
||||||
|
|
||||||
def cleanup(self):
|
def cleanup(self):
|
||||||
for attach in self.attachments:
|
for attach in self.connections:
|
||||||
attach.detach()
|
attach.detach()
|
||||||
self._remove_export()
|
self._remove_export()
|
||||||
|
|
||||||
@@ -574,7 +604,8 @@ class Connection(Object, LazyVolumeAttr):
|
|||||||
'connector': connector dictionary
|
'connector': connector dictionary
|
||||||
'device': result of connect_volume}
|
'device': result of connect_volume}
|
||||||
"""
|
"""
|
||||||
OVO_CLASS = volume_cmd.objects.VolumeAttachment
|
OVO_CLASS = cinder_objs.VolumeAttachment
|
||||||
|
SIMPLE_JSON_IGNORE = ('volume',)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def connect(cls, volume, connector, **kwargs):
|
def connect(cls, volume, connector, **kwargs):
|
||||||
@@ -613,7 +644,7 @@ class Connection(Object, LazyVolumeAttr):
|
|||||||
return connector['multipath']
|
return connector['multipath']
|
||||||
|
|
||||||
# If multipathed not defined autodetect based on connection info
|
# If multipathed not defined autodetect based on connection info
|
||||||
conn_info = conn_info['conn']['data']
|
conn_info = conn_info['conn'].get('data', {})
|
||||||
iscsi_mp = 'target_iqns' in conn_info and 'target_portals' in conn_info
|
iscsi_mp = 'target_iqns' in conn_info and 'target_portals' in conn_info
|
||||||
fc_mp = not isinstance(conn_info.get('target_wwn', ''),
|
fc_mp = not isinstance(conn_info.get('target_wwn', ''),
|
||||||
six.string_types)
|
six.string_types)
|
||||||
@@ -624,7 +655,7 @@ class Connection(Object, LazyVolumeAttr):
|
|||||||
|
|
||||||
scan_attempts = brick_initiator.DEVICE_SCAN_ATTEMPTS_DEFAULT
|
scan_attempts = brick_initiator.DEVICE_SCAN_ATTEMPTS_DEFAULT
|
||||||
self.scan_attempts = kwargs.pop('device_scan_attempts', scan_attempts)
|
self.scan_attempts = kwargs.pop('device_scan_attempts', scan_attempts)
|
||||||
volume = kwargs.pop('volume')
|
volume = kwargs.pop('volume', None)
|
||||||
self._connector = None
|
self._connector = None
|
||||||
|
|
||||||
super(Connection, self).__init__(*args, **kwargs)
|
super(Connection, self).__init__(*args, **kwargs)
|
||||||
@@ -659,6 +690,8 @@ class Connection(Object, LazyVolumeAttr):
|
|||||||
|
|
||||||
@connector_info.setter
|
@connector_info.setter
|
||||||
def connector_info(self, value):
|
def connector_info(self, value):
|
||||||
|
if self._ovo.connection_info is None:
|
||||||
|
self._ovo.connection_info = {}
|
||||||
self.connection_info['connector'] = value
|
self.connection_info['connector'] = value
|
||||||
# Since we are changing the dictionary the OVO won't detect the change
|
# Since we are changing the dictionary the OVO won't detect the change
|
||||||
self._changed_fields.add('connection_info')
|
self._changed_fields.add('connection_info')
|
||||||
@@ -801,7 +834,8 @@ class Connection(Object, LazyVolumeAttr):
|
|||||||
|
|
||||||
|
|
||||||
class Snapshot(NamedObject, LazyVolumeAttr):
|
class Snapshot(NamedObject, LazyVolumeAttr):
|
||||||
OVO_CLASS = volume_cmd.objects.Snapshot
|
OVO_CLASS = cinder_objs.Snapshot
|
||||||
|
SIMPLE_JSON_IGNORE = ('volume',)
|
||||||
DEFAULT_FIELDS_VALUES = {
|
DEFAULT_FIELDS_VALUES = {
|
||||||
'status': 'creating',
|
'status': 'creating',
|
||||||
'metadata': {},
|
'metadata': {},
|
||||||
@@ -856,8 +890,7 @@ class Snapshot(NamedObject, LazyVolumeAttr):
|
|||||||
self._ovo.update(model_update)
|
self._ovo.update(model_update)
|
||||||
except Exception:
|
except Exception:
|
||||||
self._ovo.status = 'error'
|
self._ovo.status = 'error'
|
||||||
# TODO: raise with the vol info
|
self._raise_with_resource()
|
||||||
raise
|
|
||||||
finally:
|
finally:
|
||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
@@ -865,11 +898,11 @@ class Snapshot(NamedObject, LazyVolumeAttr):
|
|||||||
try:
|
try:
|
||||||
self.backend.driver.delete_snapshot(self._ovo)
|
self.backend.driver.delete_snapshot(self._ovo)
|
||||||
self.persistence.delete_snapshot(self)
|
self.persistence.delete_snapshot(self)
|
||||||
|
self.status = 'deleted'
|
||||||
except Exception:
|
except Exception:
|
||||||
# We don't change status to error on deletion error, we assume it
|
self.status = 'error_deleting'
|
||||||
# just didn't complete.
|
self.save()
|
||||||
# TODO: raise with the snap info
|
self._raise_with_resource()
|
||||||
raise
|
|
||||||
if self._volume is not None and self._volume._snapshots is not None:
|
if self._volume is not None and self._volume._snapshots is not None:
|
||||||
try:
|
try:
|
||||||
self._volume._snapshots.remove(self)
|
self._volume._snapshots.remove(self)
|
||||||
@@ -881,6 +914,7 @@ class Snapshot(NamedObject, LazyVolumeAttr):
|
|||||||
new_vol_params.setdefault('size', self.volume_size)
|
new_vol_params.setdefault('size', self.volume_size)
|
||||||
new_vol_params['snapshot_id'] = self.id
|
new_vol_params['snapshot_id'] = self.id
|
||||||
new_vol = Volume(self.volume, **new_vol_params)
|
new_vol = Volume(self.volume, **new_vol_params)
|
||||||
|
self.backend._start_creating_volume(new_vol)
|
||||||
try:
|
try:
|
||||||
model_update = self.backend.driver.create_volume_from_snapshot(
|
model_update = self.backend.driver.create_volume_from_snapshot(
|
||||||
new_vol._ovo, self._ovo)
|
new_vol._ovo, self._ovo)
|
||||||
@@ -890,8 +924,7 @@ class Snapshot(NamedObject, LazyVolumeAttr):
|
|||||||
self.backend._volume_created(new_vol)
|
self.backend._volume_created(new_vol)
|
||||||
except Exception:
|
except Exception:
|
||||||
new_vol._ovo.status = 'error'
|
new_vol._ovo.status = 'error'
|
||||||
# TODO: raise with the new volume info
|
new_vol._raise_with_resource()
|
||||||
raise
|
|
||||||
finally:
|
finally:
|
||||||
new_vol.save()
|
new_vol.save()
|
||||||
|
|
||||||
|
|||||||
@@ -13,14 +13,17 @@
|
|||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
|
from __future__ import absolute_import
|
||||||
|
|
||||||
# NOTE(geguileo): Probably a good idea not to depend on cinder.cmd.volume
|
# NOTE(geguileo): Probably a good idea not to depend on cinder.cmd.volume
|
||||||
# having all the other imports as they could change.
|
# having all the other imports as they could change.
|
||||||
from cinder.cmd import volume as volume_cmd
|
from cinder import objects
|
||||||
from cinder.objects import base as cinder_base_ovo
|
from cinder.objects import base as cinder_base_ovo
|
||||||
from oslo_utils import timeutils
|
from oslo_utils import timeutils
|
||||||
from oslo_versionedobjects import fields
|
from oslo_versionedobjects import fields
|
||||||
import six
|
import six
|
||||||
|
|
||||||
|
import cinderlib
|
||||||
from cinderlib import serialization
|
from cinderlib import serialization
|
||||||
|
|
||||||
|
|
||||||
@@ -101,11 +104,11 @@ class PersistenceDriverBase(object):
|
|||||||
for key in resource._changed_fields
|
for key in resource._changed_fields
|
||||||
if not isinstance(resource.fields[key], fields.ObjectField)}
|
if not isinstance(resource.fields[key], fields.ObjectField)}
|
||||||
if getattr(resource._ovo, 'volume_type_id', None):
|
if getattr(resource._ovo, 'volume_type_id', None):
|
||||||
if ('qos_specs' in resource.volume_type._changed_fields
|
if ('qos_specs' in resource.volume_type._changed_fields and
|
||||||
and resource.volume_type.qos_specs):
|
resource.volume_type.qos_specs):
|
||||||
result['qos_specs'] = resource._ovo.volume_type.qos_specs.specs
|
result['qos_specs'] = resource._ovo.volume_type.qos_specs.specs
|
||||||
if ('extra_specs' in resource.volume_type._changed_fields
|
if ('extra_specs' in resource.volume_type._changed_fields and
|
||||||
and resource.volume_type.extra_specs):
|
resource.volume_type.extra_specs):
|
||||||
result['extra_specs'] = resource._ovo.volume_type.extra_specs
|
result['extra_specs'] = resource._ovo.volume_type.extra_specs
|
||||||
return result
|
return result
|
||||||
|
|
||||||
@@ -135,22 +138,24 @@ class DB(object):
|
|||||||
|
|
||||||
Data will be retrieved using the persistence driver we setup.
|
Data will be retrieved using the persistence driver we setup.
|
||||||
"""
|
"""
|
||||||
|
GET_METHODS_PER_DB_MODEL = {
|
||||||
|
objects.Volume.model: 'volume_get',
|
||||||
|
objects.VolumeType.model: 'volume_type_get',
|
||||||
|
objects.Snapshot.model: 'snapshot_get',
|
||||||
|
objects.QualityOfServiceSpecs.model: 'qos_specs_get',
|
||||||
|
}
|
||||||
|
|
||||||
def __init__(self, persistence_driver):
|
def __init__(self, persistence_driver):
|
||||||
self.persistence = persistence_driver
|
self.persistence = persistence_driver
|
||||||
|
|
||||||
# Replace the standard DB configuration for code that doesn't use the
|
|
||||||
# driver.db attribute (ie: OVOs).
|
|
||||||
volume_cmd.session.IMPL = self
|
|
||||||
|
|
||||||
# Replace get_by_id OVO methods with something that will return
|
# Replace get_by_id OVO methods with something that will return
|
||||||
# expected data
|
# expected data
|
||||||
volume_cmd.objects.Volume.get_by_id = self.volume_get
|
objects.Volume.get_by_id = self.volume_get
|
||||||
volume_cmd.objects.Snapshot.get_by_id = self.snapshot_get
|
objects.Snapshot.get_by_id = self.snapshot_get
|
||||||
|
|
||||||
# Disable saving in OVOs
|
# Disable saving in OVOs
|
||||||
for ovo_name in cinder_base_ovo.CinderObjectRegistry.obj_classes():
|
for ovo_name in cinder_base_ovo.CinderObjectRegistry.obj_classes():
|
||||||
ovo_cls = getattr(volume_cmd.objects, ovo_name)
|
ovo_cls = getattr(objects, ovo_name)
|
||||||
ovo_cls.save = lambda *args, **kwargs: None
|
ovo_cls.save = lambda *args, **kwargs: None
|
||||||
|
|
||||||
def volume_get(self, context, volume_id, *args, **kwargs):
|
def volume_get(self, context, volume_id, *args, **kwargs):
|
||||||
@@ -159,27 +164,38 @@ class DB(object):
|
|||||||
def snapshot_get(self, context, snapshot_id, *args, **kwargs):
|
def snapshot_get(self, context, snapshot_id, *args, **kwargs):
|
||||||
return self.persistence.get_snapshots(snapshot_id)[0]._ovo
|
return self.persistence.get_snapshots(snapshot_id)[0]._ovo
|
||||||
|
|
||||||
def get_volume_type(self, context, id, inactive=False,
|
def volume_type_get(self, context, id, inactive=False,
|
||||||
expected_fields=None):
|
expected_fields=None):
|
||||||
res = self.persistence.get_volumes(id)[0]._ovo
|
if id in cinderlib.Backend._volumes_inflight:
|
||||||
if not res.volume_type_id:
|
vol = cinderlib.Backend._volumes_inflight[id]
|
||||||
|
else:
|
||||||
|
vol = self.persistence.get_volumes(id)[0]
|
||||||
|
|
||||||
|
if not vol._ovo.volume_type_id:
|
||||||
return None
|
return None
|
||||||
return self._vol_type_to_dict(res.volume_type)
|
return vol_type_to_dict(vol._ovo.volume_type)
|
||||||
|
|
||||||
def qos_specs_get(self, context, qos_specs_id, inactive=False):
|
def qos_specs_get(self, context, qos_specs_id, inactive=False):
|
||||||
res = self.persistence.get_volumes(qos_specs_id)[0]._ovo
|
if qos_specs_id in cinderlib.Backend._volumes_inflight:
|
||||||
if not res.volume_type_id:
|
vol = cinderlib.Backend._volumes_inflight[qos_specs_id]
|
||||||
|
else:
|
||||||
|
vol = self.persistence.get_volumes(qos_specs_id)[0]
|
||||||
|
if not vol._ovo.volume_type_id:
|
||||||
return None
|
return None
|
||||||
return self._vol_type_to_dict(res.volume_type)['qos_specs']
|
return vol_type_to_dict(vol._ovo.volume_type)['qos_specs']
|
||||||
|
|
||||||
@staticmethod
|
@classmethod
|
||||||
def _vol_type_to_dict(volume_type):
|
def image_volume_cache_get_by_volume_id(cls, context, volume_id):
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_by_id(self, context, model, id, *args, **kwargs):
|
||||||
|
method = getattr(self, self.GET_METHODS_PER_DB_MODEL[model])
|
||||||
|
return method(context, id)
|
||||||
|
|
||||||
|
|
||||||
|
def vol_type_to_dict(volume_type):
|
||||||
res = serialization.obj_to_primitive(volume_type)
|
res = serialization.obj_to_primitive(volume_type)
|
||||||
res = res['versioned_object.data']
|
res = res['versioned_object.data']
|
||||||
if res.get('qos_specs'):
|
if res.get('qos_specs'):
|
||||||
res['qos_specs'] = res['qos_specs']['versioned_object.data']
|
res['qos_specs'] = res['qos_specs']['versioned_object.data']
|
||||||
return res
|
return res
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def image_volume_cache_get_by_volume_id(cls, context, volume_id):
|
|
||||||
return None
|
|
||||||
|
|||||||
@@ -17,12 +17,12 @@ from __future__ import absolute_import
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from cinder.cmd import volume as volume_cmd
|
|
||||||
from cinder.db import api as db_api
|
from cinder.db import api as db_api
|
||||||
from cinder.db import migration
|
from cinder.db import migration
|
||||||
from cinder.db.sqlalchemy import api as sqla_api
|
from cinder.db.sqlalchemy import api as sqla_api
|
||||||
from cinder.db.sqlalchemy import models
|
from cinder.db.sqlalchemy import models
|
||||||
from cinder import objects as cinder_objs
|
from cinder import objects as cinder_objs
|
||||||
|
from oslo_config import cfg
|
||||||
from oslo_db import exception
|
from oslo_db import exception
|
||||||
from oslo_log import log
|
from oslo_log import log
|
||||||
|
|
||||||
@@ -40,11 +40,16 @@ class KeyValue(models.BASE, models.models.ModelBase, objects.KeyValue):
|
|||||||
|
|
||||||
|
|
||||||
class DBPersistence(persistence_base.PersistenceDriverBase):
|
class DBPersistence(persistence_base.PersistenceDriverBase):
|
||||||
|
GET_METHODS_PER_DB_MODEL = {
|
||||||
|
cinder_objs.VolumeType.model: 'volume_type_get',
|
||||||
|
cinder_objs.QualityOfServiceSpecs.model: 'qos_specs_get',
|
||||||
|
}
|
||||||
|
|
||||||
def __init__(self, connection, sqlite_synchronous=True,
|
def __init__(self, connection, sqlite_synchronous=True,
|
||||||
soft_deletes=False):
|
soft_deletes=False):
|
||||||
self.soft_deletes = soft_deletes
|
self.soft_deletes = soft_deletes
|
||||||
volume_cmd.CONF.set_override('connection', connection, 'database')
|
cfg.CONF.set_override('connection', connection, 'database')
|
||||||
volume_cmd.CONF.set_override('sqlite_synchronous',
|
cfg.CONF.set_override('sqlite_synchronous',
|
||||||
sqlite_synchronous,
|
sqlite_synchronous,
|
||||||
'database')
|
'database')
|
||||||
|
|
||||||
@@ -54,20 +59,54 @@ class DBPersistence(persistence_base.PersistenceDriverBase):
|
|||||||
|
|
||||||
self._clear_facade()
|
self._clear_facade()
|
||||||
self.db_instance = db_api.oslo_db_api.DBAPI.from_config(
|
self.db_instance = db_api.oslo_db_api.DBAPI.from_config(
|
||||||
conf=volume_cmd.CONF, backend_mapping=db_api._BACKEND_MAPPING,
|
conf=cfg.CONF, backend_mapping=db_api._BACKEND_MAPPING,
|
||||||
lazy=True)
|
lazy=True)
|
||||||
|
|
||||||
|
# We need to wrap some get methods that get called before the volume is
|
||||||
|
# actually created.
|
||||||
|
self.original_vol_type_get = self.db_instance.volume_type_get
|
||||||
|
self.db_instance.volume_type_get = self.vol_type_get
|
||||||
|
self.original_qos_specs_get = self.db_instance.qos_specs_get
|
||||||
|
self.db_instance.qos_specs_get = self.qos_specs_get
|
||||||
|
self.original_get_by_id = self.db_instance.get_by_id
|
||||||
|
self.db_instance.get_by_id = self.get_by_id
|
||||||
|
|
||||||
migration.db_sync()
|
migration.db_sync()
|
||||||
self._create_key_value_table()
|
self._create_key_value_table()
|
||||||
super(DBPersistence, self).__init__()
|
super(DBPersistence, self).__init__()
|
||||||
|
|
||||||
|
def vol_type_get(self, context, id, inactive=False,
|
||||||
|
expected_fields=None):
|
||||||
|
if id not in objects.Backend._volumes_inflight:
|
||||||
|
return self.original_vol_type_get(context, id, inactive)
|
||||||
|
|
||||||
|
vol = objects.Backend._volumes_inflight[id]._ovo
|
||||||
|
if not vol.volume_type_id:
|
||||||
|
return None
|
||||||
|
return persistence_base.vol_type_to_dict(vol.volume_type)
|
||||||
|
|
||||||
|
def qos_specs_get(self, context, qos_specs_id, inactive=False):
|
||||||
|
if qos_specs_id not in objects.Backend._volumes_inflight:
|
||||||
|
return self.original_qos_specs_get(context, qos_specs_id, inactive)
|
||||||
|
|
||||||
|
vol = objects.Backend._volumes_inflight[qos_specs_id]._ovo
|
||||||
|
if not vol.volume_type_id:
|
||||||
|
return None
|
||||||
|
return persistence_base.vol_type_to_dict(vol.volume_type)['qos_specs']
|
||||||
|
|
||||||
|
def get_by_id(self, context, model, id, *args, **kwargs):
|
||||||
|
if model not in self.GET_METHODS_PER_DB_MODEL:
|
||||||
|
return self.original_get_by_id(context, model, id, *args, **kwargs)
|
||||||
|
method = getattr(self, self.GET_METHODS_PER_DB_MODEL[model])
|
||||||
|
return method(context, id)
|
||||||
|
|
||||||
def _clear_facade(self):
|
def _clear_facade(self):
|
||||||
# This is for Pike
|
# This is for Pike
|
||||||
if hasattr(sqla_api, '_FACADE'):
|
if hasattr(sqla_api, '_FACADE'):
|
||||||
sqla_api._FACADE = None
|
sqla_api._FACADE = None
|
||||||
# This is for Queens and Rocky (untested)
|
# This is for Queens and Rocky (untested)
|
||||||
elif hasattr(sqla_api, 'configure'):
|
elif hasattr(sqla_api, 'configure'):
|
||||||
sqla_api.configure(volume_cmd.CONF)
|
sqla_api.configure(cfg.CONF)
|
||||||
|
|
||||||
def _create_key_value_table(self):
|
def _create_key_value_table(self):
|
||||||
models.BASE.metadata.create_all(sqla_api.get_engine(),
|
models.BASE.metadata.create_all(sqla_api.get_engine(),
|
||||||
@@ -82,18 +121,15 @@ class DBPersistence(persistence_base.PersistenceDriverBase):
|
|||||||
return {key: value for key, value in kwargs.items() if value}
|
return {key: value for key, value in kwargs.items() if value}
|
||||||
|
|
||||||
def get_volumes(self, volume_id=None, volume_name=None, backend_name=None):
|
def get_volumes(self, volume_id=None, volume_name=None, backend_name=None):
|
||||||
if backend_name:
|
# Use the % wildcard to ignore the host name on the backend_name search
|
||||||
host = '%s@%s' % (objects.CONFIGURED_HOST, backend_name)
|
host = '%@' + backend_name if backend_name else None
|
||||||
else:
|
|
||||||
host = None
|
|
||||||
filters = self._build_filter(id=volume_id, display_name=volume_name,
|
filters = self._build_filter(id=volume_id, display_name=volume_name,
|
||||||
host=host)
|
host=host)
|
||||||
LOG.debug('get_volumes for %s', filters)
|
LOG.debug('get_volumes for %s', filters)
|
||||||
ovos = cinder_objs.VolumeList.get_all(objects.CONTEXT, filters=filters)
|
ovos = cinder_objs.VolumeList.get_all(objects.CONTEXT, filters=filters)
|
||||||
result = []
|
result = []
|
||||||
for ovo in ovos:
|
for ovo in ovos:
|
||||||
# We have stored the backend reversed with the host, switch it back
|
backend = ovo.host.split('@')[-1].split('#')[0]
|
||||||
backend = ovo.host.split('@')[1].split('#')[0]
|
|
||||||
|
|
||||||
# Trigger lazy loading of specs
|
# Trigger lazy loading of specs
|
||||||
if ovo.volume_type_id:
|
if ovo.volume_type_id:
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import six
|
|||||||
|
|
||||||
from cinder.objects import base as cinder_base_ovo
|
from cinder.objects import base as cinder_base_ovo
|
||||||
from oslo_versionedobjects import base as base_ovo
|
from oslo_versionedobjects import base as base_ovo
|
||||||
|
from oslo_versionedobjects import fields as ovo_fields
|
||||||
|
|
||||||
from cinderlib import objects
|
from cinderlib import objects
|
||||||
|
|
||||||
@@ -67,13 +68,23 @@ def wrap_to_primitive(cls):
|
|||||||
setattr(cls, 'to_primitive', staticmethod(to_primitive))
|
setattr(cls, 'to_primitive', staticmethod(to_primitive))
|
||||||
|
|
||||||
|
|
||||||
|
def _set_visited(element, visited):
|
||||||
|
# visited keeps track of elements visited to prevent loops
|
||||||
|
if visited is None:
|
||||||
|
visited = set()
|
||||||
|
# We only care about complex object that can have loops, others are ignored
|
||||||
|
# to prevent us from not serializing simple objects, such as booleans, that
|
||||||
|
# can have the same instance used for multiple fields.
|
||||||
|
if isinstance(element,
|
||||||
|
(ovo_fields.ObjectField, cinder_base_ovo.CinderObject)):
|
||||||
|
visited.add(id(element))
|
||||||
|
return visited
|
||||||
|
|
||||||
|
|
||||||
def obj_to_primitive(self, target_version=None,
|
def obj_to_primitive(self, target_version=None,
|
||||||
version_manifest=None, visited=None):
|
version_manifest=None, visited=None):
|
||||||
# No target_version, version_manifest, or changes support
|
# No target_version, version_manifest, or changes support
|
||||||
if visited is None:
|
visited = _set_visited(self, visited)
|
||||||
visited = set()
|
|
||||||
visited.add(id(self))
|
|
||||||
|
|
||||||
primitive = {}
|
primitive = {}
|
||||||
for name, field in self.fields.items():
|
for name, field in self.fields.items():
|
||||||
if self.obj_attr_is_set(name):
|
if self.obj_attr_is_set(name):
|
||||||
@@ -120,29 +131,24 @@ def field_to_primitive(self, obj, attr, value, visited=None):
|
|||||||
|
|
||||||
|
|
||||||
def iterable_to_primitive(self, obj, attr, value, visited=None):
|
def iterable_to_primitive(self, obj, attr, value, visited=None):
|
||||||
if visited is None:
|
visited = _set_visited(self, visited)
|
||||||
visited = set()
|
|
||||||
visited.add(id(value))
|
|
||||||
result = []
|
result = []
|
||||||
for elem in value:
|
for elem in value:
|
||||||
if id(elem) in visited:
|
if id(elem) in visited:
|
||||||
continue
|
continue
|
||||||
visited.add(id(elem))
|
_set_visited(elem, visited)
|
||||||
r = self._element_type.to_primitive(obj, attr, elem, visited)
|
r = self._element_type.to_primitive(obj, attr, elem, visited)
|
||||||
result.append(r)
|
result.append(r)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
def dict_to_primitive(self, obj, attr, value, visited=None):
|
def dict_to_primitive(self, obj, attr, value, visited=None):
|
||||||
if visited is None:
|
visited = _set_visited(self, visited)
|
||||||
visited = set()
|
|
||||||
visited.add(id(value))
|
|
||||||
|
|
||||||
primitive = {}
|
primitive = {}
|
||||||
for key, elem in value.items():
|
for key, elem in value.items():
|
||||||
if id(elem) in visited:
|
if id(elem) in visited:
|
||||||
continue
|
continue
|
||||||
visited.add(id(elem))
|
_set_visited(elem, visited)
|
||||||
primitive[key] = self._element_type.to_primitive(
|
primitive[key] = self._element_type.to_primitive(
|
||||||
obj, '%s["%s"]' % (attr, key), elem, visited)
|
obj, '%s["%s"]' % (attr, key), elem, visited)
|
||||||
return primitive
|
return primitive
|
||||||
|
|||||||
0
cinderlib/tests/functional/__init__.py
Normal file
0
cinderlib/tests/functional/__init__.py
Normal file
@@ -18,10 +18,13 @@ import os
|
|||||||
import subprocess
|
import subprocess
|
||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
|
from oslo_config import cfg
|
||||||
|
import six
|
||||||
import unittest2
|
import unittest2
|
||||||
import yaml
|
import yaml
|
||||||
|
|
||||||
import cinderlib
|
import cinderlib
|
||||||
|
from cinderlib.tests.functional import cinder_to_yaml
|
||||||
|
|
||||||
|
|
||||||
def set_backend(func, new_name, backend_name):
|
def set_backend(func, new_name, backend_name):
|
||||||
@@ -35,6 +38,7 @@ def set_backend(func, new_name, backend_name):
|
|||||||
|
|
||||||
|
|
||||||
def test_all_backends(cls):
|
def test_all_backends(cls):
|
||||||
|
"""Decorator to run tests in a class for all available backends."""
|
||||||
config = BaseFunctTestCase.ensure_config_loaded()
|
config = BaseFunctTestCase.ensure_config_loaded()
|
||||||
for fname, func in cls.__dict__.items():
|
for fname, func in cls.__dict__.items():
|
||||||
if fname.startswith('test_'):
|
if fname.startswith('test_'):
|
||||||
@@ -47,44 +51,55 @@ def test_all_backends(cls):
|
|||||||
|
|
||||||
|
|
||||||
class BaseFunctTestCase(unittest2.TestCase):
|
class BaseFunctTestCase(unittest2.TestCase):
|
||||||
DEFAULTS = {'logs': False, 'venv_sudo': False, 'size_precision': 0}
|
|
||||||
FNULL = open(os.devnull, 'w')
|
FNULL = open(os.devnull, 'w')
|
||||||
CONFIG_FILE = os.environ.get('CL_FTEST_CFG', 'tests/functional/lvm.yaml')
|
CONFIG_FILE = os.environ.get('CL_FTEST_CFG', '/etc/cinder/cinder.conf')
|
||||||
|
PRECISION = os.environ.get('CL_FTEST_PRECISION', 0)
|
||||||
|
LOGGING_ENABLED = os.environ.get('CL_FTEST_LOGGING', False)
|
||||||
|
ROOT_HELPER = os.environ.get('CL_FTEST_ROOT_HELPER', 'sudo')
|
||||||
tests_config = None
|
tests_config = None
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def ensure_config_loaded(cls):
|
def ensure_config_loaded(cls):
|
||||||
if not cls.tests_config:
|
if not cls.tests_config:
|
||||||
# Read backend configuration file
|
# If it's a .conf type of configuration file convert it to dict
|
||||||
|
if cls.CONFIG_FILE.endswith('.conf'):
|
||||||
|
cls.tests_config = cinder_to_yaml.convert(cls.CONFIG_FILE)
|
||||||
|
else:
|
||||||
with open(cls.CONFIG_FILE, 'r') as f:
|
with open(cls.CONFIG_FILE, 'r') as f:
|
||||||
cls.tests_config = yaml.load(f)
|
cls.tests_config = yaml.load(f)
|
||||||
# Set configuration default values
|
cls.tests_config.setdefault('logs', cls.LOGGING_ENABLED)
|
||||||
for k, v in cls.DEFAULTS.items():
|
cls.tests_config.setdefault('size_precision', cls.PRECISION)
|
||||||
cls.tests_config.setdefault(k, v)
|
|
||||||
return cls.tests_config
|
return cls.tests_config
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _replace_oslo_cli_parse():
|
||||||
|
original_cli_parser = cfg.ConfigOpts._parse_cli_opts
|
||||||
|
|
||||||
|
def _parse_cli_opts(self, args):
|
||||||
|
return original_cli_parser(self, [])
|
||||||
|
|
||||||
|
cfg.ConfigOpts._parse_cli_opts = six.create_unbound_method(
|
||||||
|
_parse_cli_opts, cfg.ConfigOpts)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpClass(cls):
|
def setUpClass(cls):
|
||||||
|
cls._replace_oslo_cli_parse()
|
||||||
config = cls.ensure_config_loaded()
|
config = cls.ensure_config_loaded()
|
||||||
|
# Use memory_db persistence instead of memory to ensure migrations work
|
||||||
if config['venv_sudo']:
|
cinderlib.setup(root_helper=cls.ROOT_HELPER,
|
||||||
# NOTE(geguileo): For some drivers need to use a custom sudo script
|
disable_logs=not config['logs'],
|
||||||
# to find virtualenv commands (ie: cinder-rtstool).
|
persistence_config={'storage': 'memory_db'})
|
||||||
path = os.path.dirname(os.path.abspath(os.path.realpath(__file__)))
|
|
||||||
sudo_tool = os.path.join(path, '../../tools/virtualenv-sudo.sh')
|
|
||||||
cls.root_helper = os.path.abspath(sudo_tool)
|
|
||||||
else:
|
|
||||||
cls.root_helper = 'sudo'
|
|
||||||
cinderlib.setup(root_helper=cls.root_helper,
|
|
||||||
disable_logs=not config['logs'])
|
|
||||||
|
|
||||||
# Initialize backends
|
# Initialize backends
|
||||||
cls.backends = [cinderlib.Backend(**cfg) for cfg in
|
cls.backends = [cinderlib.Backend(**cfg) for cfg in
|
||||||
config['backends']]
|
config['backends']]
|
||||||
|
# Lazy load backend's _volumes variable using the volumes property so
|
||||||
|
# new volumes are added to this list on successful creation.
|
||||||
|
for backend in cls.backends:
|
||||||
|
backend.volumes
|
||||||
|
|
||||||
# Set current backend, by default is the first
|
# Set current backend, by default is the first
|
||||||
cls.backend = cls.backends[0]
|
cls.backend = cls.backends[0]
|
||||||
|
|
||||||
cls.size_precision = config['size_precision']
|
cls.size_precision = config['size_precision']
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -131,7 +146,7 @@ class BaseFunctTestCase(unittest2.TestCase):
|
|||||||
raise Exception('Errors on test cleanup: %s' % '\n\t'.join(errors))
|
raise Exception('Errors on test cleanup: %s' % '\n\t'.join(errors))
|
||||||
|
|
||||||
def _root_execute(self, *args, **kwargs):
|
def _root_execute(self, *args, **kwargs):
|
||||||
cmd = [self.root_helper]
|
cmd = [self.ROOT_HELPER]
|
||||||
cmd.extend(args)
|
cmd.extend(args)
|
||||||
cmd.extend("%s=%s" % (k, v) for k, v in kwargs.items())
|
cmd.extend("%s=%s" % (k, v) for k, v in kwargs.items())
|
||||||
return subprocess.check_output(cmd, stderr=self.FNULL)
|
return subprocess.check_output(cmd, stderr=self.FNULL)
|
||||||
@@ -187,7 +202,7 @@ class BaseFunctTestCase(unittest2.TestCase):
|
|||||||
|
|
||||||
def _write_data(self, vol, data=None, do_detach=True):
|
def _write_data(self, vol, data=None, do_detach=True):
|
||||||
if not data:
|
if not data:
|
||||||
data = '0123456789' * 100
|
data = b'0123456789' * 100
|
||||||
|
|
||||||
if not vol.local_attach:
|
if not vol.local_attach:
|
||||||
vol.attach()
|
vol.attach()
|
||||||
70
cinderlib/tests/functional/cinder_to_yaml.py
Normal file
70
cinderlib/tests/functional/cinder_to_yaml.py
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
# Copyright (c) 2018, Red Hat, Inc.
|
||||||
|
# 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 os import path
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
from six.moves import configparser
|
||||||
|
|
||||||
|
from cinder.cmd import volume
|
||||||
|
volume.objects.register_all() # noqa
|
||||||
|
|
||||||
|
from cinder.volume import configuration as config
|
||||||
|
from cinder.volume import manager
|
||||||
|
|
||||||
|
|
||||||
|
def convert(cinder_source, yaml_dest=None):
|
||||||
|
result_cfgs = []
|
||||||
|
|
||||||
|
if not path.exists(cinder_source):
|
||||||
|
raise Exception("Cinder config file %s doesn't exist" % cinder_source)
|
||||||
|
|
||||||
|
# Manually parse the Cinder configuration file so we know which options are
|
||||||
|
# set.
|
||||||
|
parser = configparser.ConfigParser()
|
||||||
|
parser.read(cinder_source)
|
||||||
|
enabled_backends = parser.get('DEFAULT', 'enabled_backends')
|
||||||
|
backends = [name.strip() for name in enabled_backends.split(',') if name]
|
||||||
|
|
||||||
|
volume.CONF(('--config-file', cinder_source), project='cinder')
|
||||||
|
|
||||||
|
for backend in backends:
|
||||||
|
options_present = parser.options(backend)
|
||||||
|
|
||||||
|
# Dynamically loading the driver triggers adding the specific
|
||||||
|
# configuration options to the backend_defaults section
|
||||||
|
cfg = config.Configuration(manager.volume_backend_opts,
|
||||||
|
config_group=backend)
|
||||||
|
driver_ns = cfg.volume_driver.rsplit('.', 1)[0]
|
||||||
|
__import__(driver_ns)
|
||||||
|
|
||||||
|
# Use the backend_defaults section to extract the configuration for
|
||||||
|
# options that are present in the backend section and add them to
|
||||||
|
# the backend section.
|
||||||
|
opts = volume.CONF._groups['backend_defaults']._opts
|
||||||
|
known_present_options = [opt for opt in options_present if opt in opts]
|
||||||
|
volume_opts = [opts[option]['opt'] for option in known_present_options]
|
||||||
|
cfg.append_config_values(volume_opts)
|
||||||
|
|
||||||
|
# Now retrieve the options that are set in the configuration file.
|
||||||
|
result_cfgs.append({option: cfg.safe_get(option)
|
||||||
|
for option in known_present_options})
|
||||||
|
|
||||||
|
result = {'backends': result_cfgs}
|
||||||
|
if yaml_dest:
|
||||||
|
# Write the YAML to the destination
|
||||||
|
with open(yaml_dest, 'w') as f:
|
||||||
|
yaml.dump(result, f)
|
||||||
|
return result
|
||||||
@@ -202,15 +202,3 @@ class BackendFunctBasic(base_tests.BaseFunctTestCase):
|
|||||||
read_data = self._read_data(new_vol, len(data))
|
read_data = self._read_data(new_vol, len(data))
|
||||||
self.assertEqual(original_size, created_size)
|
self.assertEqual(original_size, created_size)
|
||||||
self.assertEqual(data, read_data)
|
self.assertEqual(data, read_data)
|
||||||
|
|
||||||
def test_connect_disconnect_volume(self):
|
|
||||||
# TODO(geguileo): Implement the test
|
|
||||||
pass
|
|
||||||
|
|
||||||
def test_connect_disconnect_multiple_volumes(self):
|
|
||||||
# TODO(geguileo): Implement the test
|
|
||||||
pass
|
|
||||||
|
|
||||||
def test_connect_disconnect_multiple_times(self):
|
|
||||||
# TODO(geguileo): Implement the test
|
|
||||||
pass
|
|
||||||
0
cinderlib/tests/unit/__init__.py
Normal file
0
cinderlib/tests/unit/__init__.py
Normal file
62
cinderlib/tests/unit/base.py
Normal file
62
cinderlib/tests/unit/base.py
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
# Copyright (c) 2018, Red Hat, Inc.
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
|
||||||
|
import mock
|
||||||
|
import unittest2
|
||||||
|
|
||||||
|
from oslo_config import cfg
|
||||||
|
import six
|
||||||
|
|
||||||
|
import cinderlib
|
||||||
|
from cinderlib.tests.unit import utils
|
||||||
|
|
||||||
|
|
||||||
|
def _replace_oslo_cli_parse():
|
||||||
|
original_cli_parser = cfg.ConfigOpts._parse_cli_opts
|
||||||
|
|
||||||
|
def _parse_cli_opts(self, args):
|
||||||
|
return original_cli_parser(self, [])
|
||||||
|
|
||||||
|
cfg.ConfigOpts._parse_cli_opts = six.create_unbound_method(_parse_cli_opts,
|
||||||
|
cfg.ConfigOpts)
|
||||||
|
|
||||||
|
|
||||||
|
_replace_oslo_cli_parse()
|
||||||
|
cinderlib.setup(persistence_config={'storage': utils.get_mock_persistence()})
|
||||||
|
|
||||||
|
|
||||||
|
class BaseTest(unittest2.TestCase):
|
||||||
|
PERSISTENCE_CFG = None
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
if not self.PERSISTENCE_CFG:
|
||||||
|
cfg = {'storage': utils.get_mock_persistence()}
|
||||||
|
cinderlib.Backend.set_persistence(cfg)
|
||||||
|
self.backend_name = 'fake_backend'
|
||||||
|
self.backend = utils.FakeBackend(volume_backend_name=self.backend_name)
|
||||||
|
self.persistence = self.backend.persistence
|
||||||
|
cinderlib.Backend._volumes_inflight = {}
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
# Clear all existing backends
|
||||||
|
cinderlib.Backend.backends = {}
|
||||||
|
|
||||||
|
def patch(self, path, *args, **kwargs):
|
||||||
|
"""Use python mock to mock a path with automatic cleanup."""
|
||||||
|
patcher = mock.patch(path, *args, **kwargs)
|
||||||
|
result = patcher.start()
|
||||||
|
self.addCleanup(patcher.stop)
|
||||||
|
return result
|
||||||
0
cinderlib/tests/unit/objects/__init__.py
Normal file
0
cinderlib/tests/unit/objects/__init__.py
Normal file
259
cinderlib/tests/unit/objects/test_connection.py
Normal file
259
cinderlib/tests/unit/objects/test_connection.py
Normal file
@@ -0,0 +1,259 @@
|
|||||||
|
# Copyright (c) 2018, Red Hat, Inc.
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
import ddt
|
||||||
|
import mock
|
||||||
|
|
||||||
|
from cinderlib import exception
|
||||||
|
from cinderlib import objects
|
||||||
|
from cinderlib.tests.unit import base
|
||||||
|
|
||||||
|
|
||||||
|
@ddt.ddt
|
||||||
|
class TestConnection(base.BaseTest):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.original_is_multipathed = objects.Connection._is_multipathed_conn
|
||||||
|
self.mock_is_mp = self.patch(
|
||||||
|
'cinderlib.objects.Connection._is_multipathed_conn')
|
||||||
|
self.mock_default = self.patch(
|
||||||
|
'os_brick.initiator.DEVICE_SCAN_ATTEMPTS_DEFAULT')
|
||||||
|
super(TestConnection, self).setUp()
|
||||||
|
|
||||||
|
self.vol = objects.Volume(self.backend_name, size=10)
|
||||||
|
self.kwargs = {'k1': 'v1', 'k2': 'v2'}
|
||||||
|
self.conn = objects.Connection(self.backend, volume=self.vol,
|
||||||
|
**self.kwargs)
|
||||||
|
self.conn._ovo.connection_info = {
|
||||||
|
'connector': {'multipath': mock.sentinel.mp_ovo_connector}}
|
||||||
|
|
||||||
|
def test_init(self):
|
||||||
|
self.mock_is_mp.assert_called_once_with(self.kwargs)
|
||||||
|
self.assertEqual(self.conn.use_multipath, self.mock_is_mp.return_value)
|
||||||
|
self.assertEqual(self.conn.scan_attempts, self.mock_default)
|
||||||
|
self.assertIsNone(self.conn._connector)
|
||||||
|
self.assertEqual(self.vol, self.conn._volume)
|
||||||
|
self.assertEqual(self.vol._ovo, self.conn._ovo.volume)
|
||||||
|
self.assertEqual(self.vol._ovo.id, self.conn._ovo.volume_id)
|
||||||
|
|
||||||
|
def test__is_multipathed_conn_kwargs(self):
|
||||||
|
res = self.original_is_multipathed(dict(
|
||||||
|
use_multipath=mock.sentinel.mp_kwargs,
|
||||||
|
connector={'multipath': mock.sentinel.mp_connector},
|
||||||
|
__ovo=self.conn._ovo))
|
||||||
|
self.assertEqual(mock.sentinel.mp_kwargs, res)
|
||||||
|
|
||||||
|
def test__is_multipathed_conn_connector_kwarg(self):
|
||||||
|
res = self.original_is_multipathed(dict(
|
||||||
|
connector={'multipath': mock.sentinel.mp_connector},
|
||||||
|
__ovo=self.conn._ovo))
|
||||||
|
self.assertEqual(mock.sentinel.mp_connector, res)
|
||||||
|
|
||||||
|
def test__is_multipathed_conn_connector_ovo(self):
|
||||||
|
res = self.original_is_multipathed(dict(connector={},
|
||||||
|
__ovo=self.conn._ovo))
|
||||||
|
self.assertEqual(mock.sentinel.mp_ovo_connector, res)
|
||||||
|
|
||||||
|
def test__is_multipathed_conn_connection_info_iscsi_true(self):
|
||||||
|
res = self.original_is_multipathed(dict(
|
||||||
|
connection_info={'conn': {'data': {'target_iqns': '',
|
||||||
|
'target_portals': ''}}}))
|
||||||
|
self.assertTrue(res)
|
||||||
|
|
||||||
|
def test__is_multipathed_conn_connection_info_iscsi_false(self):
|
||||||
|
res = self.original_is_multipathed(dict(
|
||||||
|
connection_info={'conn': {'data': {'target_iqns': ''}}}))
|
||||||
|
self.assertFalse(res)
|
||||||
|
|
||||||
|
def test__is_multipathed_conn_connection_info_fc_true(self):
|
||||||
|
res = self.original_is_multipathed(dict(
|
||||||
|
connection_info={'conn': {'data': {'target_wwn': []}}}))
|
||||||
|
self.assertTrue(res)
|
||||||
|
|
||||||
|
def test__is_multipathed_conn_connection_info_fc_false(self):
|
||||||
|
res = self.original_is_multipathed(dict(
|
||||||
|
connection_info={'conn': {'data': {'target_wwn': ''}}}))
|
||||||
|
self.assertFalse(res)
|
||||||
|
|
||||||
|
def test_init_no_backend(self):
|
||||||
|
self.assertRaises(TypeError, objects.Connection)
|
||||||
|
|
||||||
|
def test_init_no_volume(self):
|
||||||
|
self.mock_is_mp.reset_mock()
|
||||||
|
conn = objects.Connection(self.backend, **self.kwargs)
|
||||||
|
self.mock_is_mp.assert_called_once_with(self.kwargs)
|
||||||
|
self.assertEqual(conn.use_multipath, self.mock_is_mp.return_value)
|
||||||
|
self.assertEqual(conn.scan_attempts, self.mock_default)
|
||||||
|
self.assertIsNone(conn._connector)
|
||||||
|
|
||||||
|
def test_connect(self):
|
||||||
|
connector = {'my_c': 'v'}
|
||||||
|
conn = self.conn.connect(self.vol, connector)
|
||||||
|
init_conn = self.backend.driver.initialize_connection
|
||||||
|
init_conn.assert_called_once_with(self.vol, connector)
|
||||||
|
self.assertIsInstance(conn, objects.Connection)
|
||||||
|
self.assertEqual('attached', conn.status)
|
||||||
|
self.assertEqual(init_conn.return_value, conn.connection_info['conn'])
|
||||||
|
self.assertEqual(connector, conn.connector_info)
|
||||||
|
self.persistence.set_connection.assert_called_once_with(conn)
|
||||||
|
|
||||||
|
@mock.patch('cinderlib.objects.Volume._disconnect')
|
||||||
|
@mock.patch('cinderlib.objects.Connection._disconnect')
|
||||||
|
def test_disconnect(self, mock_disc, mock_vol_disc):
|
||||||
|
self.conn.disconnect(force=mock.sentinel.force)
|
||||||
|
mock_disc.assert_called_once_with(mock.sentinel.force)
|
||||||
|
mock_vol_disc.assert_called_once_with(self.conn)
|
||||||
|
|
||||||
|
def test__disconnect(self):
|
||||||
|
conn_info = self.conn.connector_info
|
||||||
|
self.conn._disconnect(mock.sentinel.force)
|
||||||
|
self.backend.driver.terminate_connection.assert_called_once_with(
|
||||||
|
self.vol._ovo, conn_info, force=mock.sentinel.force)
|
||||||
|
self.assertEqual({}, self.conn.conn_info)
|
||||||
|
self.assertEqual('detached', self.conn.status)
|
||||||
|
self.persistence.delete_connection.assert_called_once_with(self.conn)
|
||||||
|
|
||||||
|
@mock.patch('cinderlib.objects.Connection.conn_info', {'data': 'mydata'})
|
||||||
|
@mock.patch('cinderlib.objects.Connection.path')
|
||||||
|
@mock.patch('cinderlib.objects.Connection.device_attached')
|
||||||
|
def test_attach(self, mock_attached, mock_path):
|
||||||
|
with mock.patch('cinderlib.objects.Connection.connector') as mock_conn:
|
||||||
|
self.conn.attach()
|
||||||
|
mock_conn.connect_volume.assert_called_once_with('mydata')
|
||||||
|
mock_attached.assert_called_once_with(
|
||||||
|
mock_conn.connect_volume.return_value)
|
||||||
|
mock_conn.check_valid_device.assert_called_once_with(mock_path)
|
||||||
|
self.assertEqual(self.conn, self.vol.local_attach)
|
||||||
|
|
||||||
|
@mock.patch('cinderlib.objects.Connection.conn_info', {'data': 'mydata'})
|
||||||
|
@mock.patch('cinderlib.objects.Connection.device')
|
||||||
|
def test_detach(self, mock_device):
|
||||||
|
self.vol.local_attach = mock.Mock()
|
||||||
|
with mock.patch('cinderlib.objects.Connection.connector') as mock_conn:
|
||||||
|
self.conn.detach(mock.sentinel.force, mock.sentinel.ignore)
|
||||||
|
mock_conn.disconnect_volume.assert_called_once_with(
|
||||||
|
'mydata',
|
||||||
|
mock_device,
|
||||||
|
force=mock.sentinel.force,
|
||||||
|
ignore_errors=mock.sentinel.ignore)
|
||||||
|
self.assertIsNone(self.vol.local_attach)
|
||||||
|
self.assertIsNone(self.conn.device)
|
||||||
|
self.assertIsNone(self.conn._connector)
|
||||||
|
self.persistence.set_connection.assert_called_once_with(self.conn)
|
||||||
|
|
||||||
|
def test_get_by_id(self):
|
||||||
|
self.persistence.get_connections.return_value = [mock.sentinel.conn]
|
||||||
|
res = objects.Connection.get_by_id(mock.sentinel.conn_id)
|
||||||
|
self.assertEqual(mock.sentinel.conn, res)
|
||||||
|
self.persistence.get_connections.assert_called_once_with(
|
||||||
|
connection_id=mock.sentinel.conn_id)
|
||||||
|
|
||||||
|
def test_get_by_id_not_found(self):
|
||||||
|
self.persistence.get_connections.return_value = None
|
||||||
|
self.assertRaises(exception.ConnectionNotFound,
|
||||||
|
objects.Connection.get_by_id,
|
||||||
|
mock.sentinel.conn_id)
|
||||||
|
self.persistence.get_connections.assert_called_once_with(
|
||||||
|
connection_id=mock.sentinel.conn_id)
|
||||||
|
|
||||||
|
def test_device_attached(self):
|
||||||
|
self.conn.device_attached(mock.sentinel.device)
|
||||||
|
self.assertEqual(mock.sentinel.device,
|
||||||
|
self.conn.connection_info['device'])
|
||||||
|
self.persistence.set_connection.assert_called_once_with(self.conn)
|
||||||
|
|
||||||
|
def test_conn_info_setter(self):
|
||||||
|
self.conn.conn_info = mock.sentinel.conn_info
|
||||||
|
self.assertEqual(mock.sentinel.conn_info,
|
||||||
|
self.conn._ovo.connection_info['conn'])
|
||||||
|
|
||||||
|
def test_conn_info_setter_clear(self):
|
||||||
|
self.conn.conn_info = mock.sentinel.conn_info
|
||||||
|
self.conn.conn_info = {}
|
||||||
|
self.assertIsNone(self.conn._ovo.connection_info)
|
||||||
|
|
||||||
|
def test_conn_info_getter(self):
|
||||||
|
self.conn.conn_info = mock.sentinel.conn_info
|
||||||
|
self.assertEqual(mock.sentinel.conn_info, self.conn.conn_info)
|
||||||
|
|
||||||
|
def test_conn_info_getter_none(self):
|
||||||
|
self.conn.conn_info = None
|
||||||
|
self.assertEqual({}, self.conn.conn_info)
|
||||||
|
|
||||||
|
def test_protocol(self):
|
||||||
|
self.conn.conn_info = {'driver_volume_type': mock.sentinel.iscsi}
|
||||||
|
self.assertEqual(mock.sentinel.iscsi, self.conn.protocol)
|
||||||
|
|
||||||
|
def test_connector_info_setter(self):
|
||||||
|
self.conn.connector_info = mock.sentinel.connector
|
||||||
|
self.assertEqual(mock.sentinel.connector,
|
||||||
|
self.conn._ovo.connection_info['connector'])
|
||||||
|
self.assertIn('connection_info', self.conn._ovo._changed_fields)
|
||||||
|
|
||||||
|
def test_connector_info_getter(self):
|
||||||
|
self.conn.connector_info = mock.sentinel.connector
|
||||||
|
self.assertEqual(mock.sentinel.connector, self.conn.connector_info)
|
||||||
|
|
||||||
|
def test_connector_info_getter_empty(self):
|
||||||
|
self.conn._ovo.connection_info = None
|
||||||
|
self.assertIsNone(self.conn.connector_info)
|
||||||
|
|
||||||
|
def test_device_setter(self):
|
||||||
|
self.conn.device = mock.sentinel.device
|
||||||
|
self.assertEqual(mock.sentinel.device,
|
||||||
|
self.conn._ovo.connection_info['device'])
|
||||||
|
self.assertIn('connection_info', self.conn._ovo._changed_fields)
|
||||||
|
|
||||||
|
def test_device_setter_none(self):
|
||||||
|
self.conn.device = mock.sentinel.device
|
||||||
|
self.conn.device = None
|
||||||
|
self.assertNotIn('device', self.conn._ovo.connection_info)
|
||||||
|
self.assertIn('connection_info', self.conn._ovo._changed_fields)
|
||||||
|
|
||||||
|
def test_device_getter(self):
|
||||||
|
self.conn.device = mock.sentinel.device
|
||||||
|
self.assertEqual(mock.sentinel.device, self.conn.device)
|
||||||
|
|
||||||
|
def test_path(self):
|
||||||
|
self.conn.device = {'path': mock.sentinel.path}
|
||||||
|
self.assertEqual(mock.sentinel.path, self.conn.path)
|
||||||
|
|
||||||
|
@mock.patch('cinderlib.objects.Connection.conn_info')
|
||||||
|
@mock.patch('cinderlib.objects.Connection.protocol')
|
||||||
|
@mock.patch('os_brick.initiator.connector.InitiatorConnector.factory')
|
||||||
|
def test_connector_getter(self, mock_connector, mock_proto, mock_info):
|
||||||
|
res = self.conn.connector
|
||||||
|
self.assertEqual(mock_connector.return_value, res)
|
||||||
|
mock_connector.assert_called_once_with(
|
||||||
|
mock_proto,
|
||||||
|
self.backend.root_helper,
|
||||||
|
use_multipath=self.mock_is_mp.return_value,
|
||||||
|
device_scan_attempts=self.mock_default,
|
||||||
|
conn=mock_info,
|
||||||
|
do_local_attach=True)
|
||||||
|
|
||||||
|
# Make sure we cache the value
|
||||||
|
res = self.conn.connector
|
||||||
|
self.assertEqual(1, mock_connector.call_count)
|
||||||
|
|
||||||
|
@ddt.data(True, False)
|
||||||
|
def test_attached_true(self, value):
|
||||||
|
with mock.patch('cinderlib.objects.Connection.device', value):
|
||||||
|
self.assertEqual(value, self.conn.attached)
|
||||||
|
|
||||||
|
@ddt.data(True, False)
|
||||||
|
def test_connected(self, value):
|
||||||
|
with mock.patch('cinderlib.objects.Connection.conn_info', value):
|
||||||
|
self.assertEqual(value, self.conn.connected)
|
||||||
146
cinderlib/tests/unit/objects/test_snapshot.py
Normal file
146
cinderlib/tests/unit/objects/test_snapshot.py
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
# Copyright (c) 2018, Red Hat, Inc.
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
import mock
|
||||||
|
|
||||||
|
from cinderlib import exception
|
||||||
|
from cinderlib import objects
|
||||||
|
from cinderlib.tests.unit import base
|
||||||
|
|
||||||
|
|
||||||
|
class TestSnapshot(base.BaseTest):
|
||||||
|
def setUp(self):
|
||||||
|
super(TestSnapshot, self).setUp()
|
||||||
|
self.vol = objects.Volume(self.backend_name, size=10,
|
||||||
|
extra_specs={'e': 'v'},
|
||||||
|
qos_specs={'q': 'qv'})
|
||||||
|
self.snap = objects.Snapshot(self.vol,
|
||||||
|
name='my_snap', description='my_desc')
|
||||||
|
self.vol._snapshots.append(self.snap)
|
||||||
|
self.vol._ovo.snapshots.objects.append(self.snap._ovo)
|
||||||
|
|
||||||
|
def test_init_from_volume(self):
|
||||||
|
self.assertIsNotNone(self.snap.id)
|
||||||
|
self.assertEqual(self.backend, self.snap.backend)
|
||||||
|
self.assertEqual('my_snap', self.snap.name)
|
||||||
|
self.assertEqual('my_snap', self.snap.display_name)
|
||||||
|
self.assertEqual('my_desc', self.snap.description)
|
||||||
|
self.assertEqual(self.vol.user_id, self.snap.user_id)
|
||||||
|
self.assertEqual(self.vol.project_id, self.snap.project_id)
|
||||||
|
self.assertEqual(self.vol.id, self.snap.volume_id)
|
||||||
|
self.assertEqual(self.vol.size, self.snap.volume_size)
|
||||||
|
self.assertEqual(self.vol._ovo, self.snap._ovo.volume)
|
||||||
|
self.assertEqual(self.vol.volume_type_id, self.snap.volume_type_id)
|
||||||
|
self.assertEqual(self.vol, self.snap.volume)
|
||||||
|
|
||||||
|
def test_init_from_ovo(self):
|
||||||
|
snap2 = objects.Snapshot(None, __ovo=self.snap._ovo)
|
||||||
|
self.assertEqual(self.snap.backend, snap2.backend)
|
||||||
|
self.assertEqual(self.snap._ovo, snap2._ovo)
|
||||||
|
self.assertEqual(self.vol, self.snap.volume)
|
||||||
|
|
||||||
|
def test_create(self):
|
||||||
|
update_vol = {'provider_id': 'provider_id'}
|
||||||
|
self.backend.driver.create_snapshot.return_value = update_vol
|
||||||
|
self.snap.create()
|
||||||
|
self.assertEqual('available', self.snap.status)
|
||||||
|
self.assertEqual('provider_id', self.snap.provider_id)
|
||||||
|
self.backend.driver.create_snapshot.assert_called_once_with(
|
||||||
|
self.snap._ovo)
|
||||||
|
self.persistence.set_snapshot.assert_called_once_with(self.snap)
|
||||||
|
|
||||||
|
def test_create_error(self):
|
||||||
|
self.backend.driver.create_snapshot.side_effect = exception.NotFound
|
||||||
|
with self.assertRaises(exception.NotFound) as assert_context:
|
||||||
|
self.snap.create()
|
||||||
|
|
||||||
|
self.assertEqual(self.snap, assert_context.exception.resource)
|
||||||
|
self.backend.driver.create_snapshot.assert_called_once_with(
|
||||||
|
self.snap._ovo)
|
||||||
|
self.assertEqual('error', self.snap.status)
|
||||||
|
self.persistence.set_snapshot.assert_called_once_with(self.snap)
|
||||||
|
|
||||||
|
def test_delete(self):
|
||||||
|
self.snap.delete()
|
||||||
|
self.backend.driver.delete_snapshot.assert_called_once_with(
|
||||||
|
self.snap._ovo)
|
||||||
|
self.persistence.delete_snapshot.assert_called_once_with(self.snap)
|
||||||
|
self.assertEqual([], self.vol.snapshots)
|
||||||
|
self.assertEqual([], self.vol._ovo.snapshots.objects)
|
||||||
|
self.assertEqual('deleted', self.snap.status)
|
||||||
|
|
||||||
|
def test_delete_error(self):
|
||||||
|
self.backend.driver.delete_snapshot.side_effect = exception.NotFound
|
||||||
|
with self.assertRaises(exception.NotFound) as assert_context:
|
||||||
|
self.snap.delete()
|
||||||
|
self.assertEqual(self.snap, assert_context.exception.resource)
|
||||||
|
self.backend.driver.delete_snapshot.assert_called_once_with(
|
||||||
|
self.snap._ovo)
|
||||||
|
self.persistence.delete_snapshot.assert_not_called()
|
||||||
|
self.assertEqual([self.snap], self.vol.snapshots)
|
||||||
|
self.assertEqual([self.snap._ovo], self.vol._ovo.snapshots.objects)
|
||||||
|
self.assertEqual('error_deleting', self.snap.status)
|
||||||
|
|
||||||
|
def test_create_volume(self):
|
||||||
|
create_mock = self.backend.driver.create_volume_from_snapshot
|
||||||
|
create_mock.return_value = None
|
||||||
|
vol2 = self.snap.create_volume(name='new_name', description='new_desc')
|
||||||
|
create_mock.assert_called_once_with(vol2._ovo, self.snap._ovo)
|
||||||
|
self.assertEqual('available', vol2.status)
|
||||||
|
self.assertEqual(1, len(self.backend._volumes))
|
||||||
|
self.assertEqual(vol2, self.backend._volumes[0])
|
||||||
|
self.persistence.set_volume.assert_called_once_with(vol2)
|
||||||
|
self.assertEqual(self.vol.id, self.vol.volume_type_id)
|
||||||
|
self.assertNotEqual(self.vol.id, vol2.id)
|
||||||
|
self.assertEqual(vol2.id, vol2.volume_type_id)
|
||||||
|
self.assertEqual(self.vol.volume_type.extra_specs,
|
||||||
|
vol2.volume_type.extra_specs)
|
||||||
|
self.assertEqual(self.vol.volume_type.qos_specs.specs,
|
||||||
|
vol2.volume_type.qos_specs.specs)
|
||||||
|
|
||||||
|
def test_create_volume_error(self):
|
||||||
|
create_mock = self.backend.driver.create_volume_from_snapshot
|
||||||
|
create_mock.side_effect = exception.NotFound
|
||||||
|
with self.assertRaises(exception.NotFound) as assert_context:
|
||||||
|
self.snap.create_volume()
|
||||||
|
self.assertEqual(1, len(self.backend._volumes_inflight))
|
||||||
|
vol2 = list(self.backend._volumes_inflight.values())[0]
|
||||||
|
self.assertEqual(vol2, assert_context.exception.resource)
|
||||||
|
create_mock.assert_called_once_with(vol2, self.snap._ovo)
|
||||||
|
self.assertEqual('error', vol2.status)
|
||||||
|
self.persistence.set_volume.assert_called_once_with(mock.ANY)
|
||||||
|
|
||||||
|
def test_get_by_id(self):
|
||||||
|
mock_get_snaps = self.persistence.get_snapshots
|
||||||
|
mock_get_snaps.return_value = [mock.sentinel.snap]
|
||||||
|
res = objects.Snapshot.get_by_id(mock.sentinel.snap_id)
|
||||||
|
mock_get_snaps.assert_called_once_with(
|
||||||
|
snapshot_id=mock.sentinel.snap_id)
|
||||||
|
self.assertEqual(mock.sentinel.snap, res)
|
||||||
|
|
||||||
|
def test_get_by_id_not_found(self):
|
||||||
|
mock_get_snaps = self.persistence.get_snapshots
|
||||||
|
mock_get_snaps.return_value = None
|
||||||
|
self.assertRaises(exception.SnapshotNotFound,
|
||||||
|
objects.Snapshot.get_by_id, mock.sentinel.snap_id)
|
||||||
|
mock_get_snaps.assert_called_once_with(
|
||||||
|
snapshot_id=mock.sentinel.snap_id)
|
||||||
|
|
||||||
|
def test_get_by_name(self):
|
||||||
|
res = objects.Snapshot.get_by_name(mock.sentinel.name)
|
||||||
|
mock_get_snaps = self.persistence.get_snapshots
|
||||||
|
mock_get_snaps.assert_called_once_with(
|
||||||
|
snapshot_name=mock.sentinel.name)
|
||||||
|
self.assertEqual(mock_get_snaps.return_value, res)
|
||||||
428
cinderlib/tests/unit/objects/test_volume.py
Normal file
428
cinderlib/tests/unit/objects/test_volume.py
Normal file
@@ -0,0 +1,428 @@
|
|||||||
|
# Copyright (c) 2018, Red Hat, Inc.
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
import mock
|
||||||
|
|
||||||
|
from cinderlib import exception
|
||||||
|
from cinderlib import objects
|
||||||
|
from cinderlib.tests.unit import base
|
||||||
|
|
||||||
|
|
||||||
|
class TestVolume(base.BaseTest):
|
||||||
|
def test_init_from_args_backend_name(self):
|
||||||
|
vol = objects.Volume(self.backend_name,
|
||||||
|
name='vol_name', description='vol_desc', size=10)
|
||||||
|
self.assertEqual(self.backend, vol.backend)
|
||||||
|
self.assertEqual('vol_name', vol.name)
|
||||||
|
self.assertEqual('vol_name', vol.display_name)
|
||||||
|
self.assertEqual('vol_desc', vol.description)
|
||||||
|
self.assertEqual(10, vol.size)
|
||||||
|
self.assertIsNotNone(vol.id)
|
||||||
|
|
||||||
|
def test_init_from_args_backend(self):
|
||||||
|
vol = objects.Volume(self.backend,
|
||||||
|
name='vol_name', description='vol_desc', size=10)
|
||||||
|
self.assertEqual(self.backend, vol.backend)
|
||||||
|
self.assertEqual('vol_name', vol.name)
|
||||||
|
self.assertEqual('vol_name', vol.display_name)
|
||||||
|
self.assertEqual('vol_desc', vol.description)
|
||||||
|
self.assertEqual(10, vol.size)
|
||||||
|
self.assertIsNotNone(vol.id)
|
||||||
|
|
||||||
|
def test_init_from_volume(self):
|
||||||
|
vol = objects.Volume(self.backend,
|
||||||
|
name='vol_name', description='vol_desc', size=10)
|
||||||
|
vol2 = objects.Volume(vol, name='new_name', size=11)
|
||||||
|
self.assertEqual(self.backend, vol2.backend)
|
||||||
|
self.assertEqual('new_name', vol2.name)
|
||||||
|
self.assertEqual('new_name', vol2.display_name)
|
||||||
|
self.assertEqual(vol.description, vol2.description)
|
||||||
|
self.assertEqual(11, vol2.size)
|
||||||
|
self.assertIsNotNone(vol2.id)
|
||||||
|
self.assertNotEqual(vol.id, vol2.id)
|
||||||
|
|
||||||
|
def test_init_from_ovo(self):
|
||||||
|
vol = objects.Volume(self.backend, size=10)
|
||||||
|
vol2 = objects.Volume(self.backend, __ovo=vol._ovo)
|
||||||
|
self.assertEqual(vol._ovo, vol2._ovo)
|
||||||
|
|
||||||
|
def test_snapshots_lazy_loading(self):
|
||||||
|
vol = objects.Volume(self.backend, size=10)
|
||||||
|
vol._snapshots = None
|
||||||
|
|
||||||
|
snaps = [objects.Snapshot(vol, name='my_snap')]
|
||||||
|
# Persistence retrieves Snapshots without the Volume, just volume_id
|
||||||
|
snaps[0]._ovo.volume = None
|
||||||
|
|
||||||
|
mock_get_snaps = self.persistence.get_snapshots
|
||||||
|
mock_get_snaps.return_value = snaps
|
||||||
|
|
||||||
|
result = vol.snapshots
|
||||||
|
|
||||||
|
mock_get_snaps.called_once_with(vol.id)
|
||||||
|
self.assertEqual(snaps, result)
|
||||||
|
self.assertEqual(snaps, vol._snapshots)
|
||||||
|
self.assertEqual(1, len(vol._ovo.snapshots))
|
||||||
|
self.assertEqual(vol._ovo.snapshots[0], result[0]._ovo)
|
||||||
|
# There is no second call when we reference it again
|
||||||
|
mock_get_snaps.reset_mock()
|
||||||
|
result = vol.snapshots
|
||||||
|
self.assertEqual(snaps, result)
|
||||||
|
mock_get_snaps.not_called()
|
||||||
|
|
||||||
|
def test_connections_lazy_loading(self):
|
||||||
|
vol = objects.Volume(self.backend, size=10)
|
||||||
|
vol._connections = None
|
||||||
|
|
||||||
|
conns = [objects.Connection(self.backend, connector={'k': 'v'},
|
||||||
|
volume_id=vol.id, status='attached',
|
||||||
|
attach_mode='rw',
|
||||||
|
connection_info={'conn': {}},
|
||||||
|
name='my_snap')]
|
||||||
|
|
||||||
|
mock_get_conns = self.persistence.get_connections
|
||||||
|
mock_get_conns.return_value = conns
|
||||||
|
|
||||||
|
result = vol.connections
|
||||||
|
|
||||||
|
mock_get_conns.called_once_with(volume_id=vol.id)
|
||||||
|
self.assertEqual(conns, result)
|
||||||
|
self.assertEqual(conns, vol._connections)
|
||||||
|
self.assertEqual(1, len(vol._ovo.volume_attachment))
|
||||||
|
self.assertEqual(vol._ovo.volume_attachment[0], result[0]._ovo)
|
||||||
|
# There is no second call when we reference it again
|
||||||
|
mock_get_conns.reset_mock()
|
||||||
|
result = vol.connections
|
||||||
|
self.assertEqual(conns, result)
|
||||||
|
mock_get_conns.not_called()
|
||||||
|
|
||||||
|
def test_get_by_id(self):
|
||||||
|
mock_get_vols = self.persistence.get_volumes
|
||||||
|
mock_get_vols.return_value = [mock.sentinel.vol]
|
||||||
|
res = objects.Volume.get_by_id(mock.sentinel.vol_id)
|
||||||
|
mock_get_vols.assert_called_once_with(volume_id=mock.sentinel.vol_id)
|
||||||
|
self.assertEqual(mock.sentinel.vol, res)
|
||||||
|
|
||||||
|
def test_get_by_id_not_found(self):
|
||||||
|
mock_get_vols = self.persistence.get_volumes
|
||||||
|
mock_get_vols.return_value = None
|
||||||
|
self.assertRaises(exception.VolumeNotFound,
|
||||||
|
objects.Volume.get_by_id, mock.sentinel.vol_id)
|
||||||
|
mock_get_vols.assert_called_once_with(volume_id=mock.sentinel.vol_id)
|
||||||
|
|
||||||
|
def test_get_by_name(self):
|
||||||
|
res = objects.Volume.get_by_name(mock.sentinel.name)
|
||||||
|
mock_get_vols = self.persistence.get_volumes
|
||||||
|
mock_get_vols.assert_called_once_with(volume_name=mock.sentinel.name)
|
||||||
|
self.assertEqual(mock_get_vols.return_value, res)
|
||||||
|
|
||||||
|
def test_create(self):
|
||||||
|
self.backend.driver.create_volume.return_value = None
|
||||||
|
vol = self.backend.create_volume(10, name='vol_name',
|
||||||
|
description='des')
|
||||||
|
self.backend.driver.create_volume.assert_called_once_with(vol._ovo)
|
||||||
|
self.assertEqual('available', vol.status)
|
||||||
|
self.persistence.set_volume.assert_called_once_with(vol)
|
||||||
|
|
||||||
|
def test_create_error(self):
|
||||||
|
self.backend.driver.create_volume.side_effect = exception.NotFound
|
||||||
|
with self.assertRaises(exception.NotFound) as assert_context:
|
||||||
|
self.backend.create_volume(10, name='vol_name', description='des')
|
||||||
|
vol = assert_context.exception.resource
|
||||||
|
self.assertIsInstance(vol, objects.Volume)
|
||||||
|
self.assertEqual(10, vol.size)
|
||||||
|
self.assertEqual('vol_name', vol.name)
|
||||||
|
self.assertEqual('des', vol.description)
|
||||||
|
|
||||||
|
def test_delete(self):
|
||||||
|
vol = objects.Volume(self.backend_name, size=10)
|
||||||
|
vol.delete()
|
||||||
|
self.backend.driver.delete_volume.assert_called_once_with(vol._ovo)
|
||||||
|
self.persistence.delete_volume.assert_called_once_with(vol)
|
||||||
|
self.assertEqual('deleted', vol.status)
|
||||||
|
|
||||||
|
def test_delete_error_with_snaps(self):
|
||||||
|
vol = objects.Volume(self.backend_name, size=10, status='available')
|
||||||
|
snap = objects.Snapshot(vol)
|
||||||
|
vol._snapshots.append(snap)
|
||||||
|
self.assertRaises(exception.InvalidVolume, vol.delete)
|
||||||
|
self.assertEqual('available', vol.status)
|
||||||
|
|
||||||
|
def test_delete_error(self):
|
||||||
|
vol = objects.Volume(self.backend_name,
|
||||||
|
name='vol_name', description='vol_desc', size=10)
|
||||||
|
self.backend.driver.delete_volume.side_effect = exception.NotFound
|
||||||
|
with self.assertRaises(exception.NotFound) as assert_context:
|
||||||
|
vol.delete()
|
||||||
|
|
||||||
|
self.assertEqual(vol, assert_context.exception.resource)
|
||||||
|
self.backend.driver.delete_volume.assert_called_once_with(vol._ovo)
|
||||||
|
self.assertEqual('error_deleting', vol.status)
|
||||||
|
|
||||||
|
def test_extend(self):
|
||||||
|
vol = objects.Volume(self.backend_name, status='available', size=10)
|
||||||
|
vol.extend(11)
|
||||||
|
|
||||||
|
self.backend.driver.extend_volume.assert_called_once_with(vol._ovo, 11)
|
||||||
|
self.persistence.set_volume.assert_called_once_with(vol)
|
||||||
|
self.assertEqual('available', vol.status)
|
||||||
|
self.assertEqual(11, vol.size)
|
||||||
|
|
||||||
|
def test_extend_error(self):
|
||||||
|
vol = objects.Volume(self.backend_name, status='available', size=10)
|
||||||
|
self.backend.driver.extend_volume.side_effect = exception.NotFound
|
||||||
|
with self.assertRaises(exception.NotFound) as assert_context:
|
||||||
|
vol.extend(11)
|
||||||
|
|
||||||
|
self.assertEqual(vol, assert_context.exception.resource)
|
||||||
|
self.backend.driver.extend_volume.assert_called_once_with(vol._ovo, 11)
|
||||||
|
self.persistence.set_volume.assert_called_once_with(vol)
|
||||||
|
self.assertEqual('error', vol.status)
|
||||||
|
self.assertEqual(10, vol.size)
|
||||||
|
|
||||||
|
def test_clone(self):
|
||||||
|
vol = objects.Volume(self.backend_name, status='available', size=10,
|
||||||
|
extra_specs={'e': 'v'}, qos_specs={'q': 'qv'})
|
||||||
|
mock_clone = self.backend.driver.create_cloned_volume
|
||||||
|
mock_clone.return_value = None
|
||||||
|
|
||||||
|
res = vol.clone(size=11)
|
||||||
|
|
||||||
|
mock_clone.assert_called_once_with(res._ovo, vol._ovo)
|
||||||
|
self.persistence.set_volume.assert_called_once_with(res)
|
||||||
|
self.assertEqual('available', res.status)
|
||||||
|
self.assertEqual(11, res.size)
|
||||||
|
self.assertEqual(vol.id, vol.volume_type_id)
|
||||||
|
self.assertNotEqual(vol.id, res.id)
|
||||||
|
self.assertEqual(res.id, res.volume_type_id)
|
||||||
|
self.assertEqual(vol.volume_type.extra_specs,
|
||||||
|
res.volume_type.extra_specs)
|
||||||
|
self.assertEqual(vol.volume_type.qos_specs.specs,
|
||||||
|
res.volume_type.qos_specs.specs)
|
||||||
|
|
||||||
|
def test_clone_error(self):
|
||||||
|
vol = objects.Volume(self.backend_name, status='available', size=10)
|
||||||
|
|
||||||
|
mock_clone = self.backend.driver.create_cloned_volume
|
||||||
|
mock_clone.side_effect = exception.NotFound
|
||||||
|
|
||||||
|
with self.assertRaises(exception.NotFound) as assert_context:
|
||||||
|
vol.clone(size=11)
|
||||||
|
|
||||||
|
# Cloning volume is still in flight
|
||||||
|
self.assertEqual(1, len(self.backend._volumes_inflight))
|
||||||
|
new_vol = list(self.backend._volumes_inflight.values())[0]
|
||||||
|
self.assertEqual(new_vol, assert_context.exception.resource)
|
||||||
|
mock_clone.assert_called_once_with(new_vol, vol._ovo)
|
||||||
|
|
||||||
|
self.persistence.set_volume.assert_called_once_with(new_vol)
|
||||||
|
self.assertEqual('error', new_vol.status)
|
||||||
|
self.assertEqual(11, new_vol.size)
|
||||||
|
|
||||||
|
def test_create_snapshot(self):
|
||||||
|
vol = objects.Volume(self.backend_name, status='available', size=10)
|
||||||
|
mock_create = self.backend.driver.create_snapshot
|
||||||
|
mock_create.return_value = None
|
||||||
|
|
||||||
|
snap = vol.create_snapshot()
|
||||||
|
|
||||||
|
self.assertEqual([snap], vol.snapshots)
|
||||||
|
self.assertEqual([snap._ovo], vol._ovo.snapshots.objects)
|
||||||
|
mock_create.assert_called_once_with(snap._ovo)
|
||||||
|
self.assertEqual('available', snap.status)
|
||||||
|
self.assertEqual(10, snap.volume_size)
|
||||||
|
self.persistence.set_snapshot.assert_called_once_with(snap)
|
||||||
|
|
||||||
|
def test_create_snapshot_error(self):
|
||||||
|
vol = objects.Volume(self.backend_name, status='available', size=10)
|
||||||
|
mock_create = self.backend.driver.create_snapshot
|
||||||
|
mock_create.side_effect = exception.NotFound
|
||||||
|
|
||||||
|
self.assertRaises(exception.NotFound, vol.create_snapshot)
|
||||||
|
|
||||||
|
self.assertEqual(1, len(vol.snapshots))
|
||||||
|
snap = vol.snapshots[0]
|
||||||
|
self.persistence.set_snapshot.assert_called_once_with(snap)
|
||||||
|
self.assertEqual('error', snap.status)
|
||||||
|
mock_create.assert_called_once_with(snap._ovo)
|
||||||
|
|
||||||
|
@mock.patch('os_brick.initiator.connector.get_connector_properties')
|
||||||
|
@mock.patch('cinderlib.objects.Volume.connect')
|
||||||
|
def test_attach(self, mock_connect, mock_conn_props):
|
||||||
|
vol = objects.Volume(self.backend_name, status='available', size=10)
|
||||||
|
res = vol.attach()
|
||||||
|
|
||||||
|
mock_conn_props.assert_called_once_with(
|
||||||
|
self.backend.root_helper,
|
||||||
|
mock.ANY,
|
||||||
|
self.backend.configuration.use_multipath_for_image_xfer,
|
||||||
|
self.backend.configuration.enforce_multipath_for_image_xfer)
|
||||||
|
|
||||||
|
mock_connect.assert_called_once_with(mock_conn_props.return_value)
|
||||||
|
mock_connect.return_value.attach.assert_called_once_with()
|
||||||
|
self.assertEqual(mock_connect.return_value, res)
|
||||||
|
|
||||||
|
@mock.patch('os_brick.initiator.connector.get_connector_properties')
|
||||||
|
@mock.patch('cinderlib.objects.Volume.connect')
|
||||||
|
def test_attach_error_connect(self, mock_connect, mock_conn_props):
|
||||||
|
vol = objects.Volume(self.backend_name, status='available', size=10)
|
||||||
|
mock_connect.side_effect = exception.NotFound
|
||||||
|
|
||||||
|
self.assertRaises(exception.NotFound, vol.attach)
|
||||||
|
|
||||||
|
mock_conn_props.assert_called_once_with(
|
||||||
|
self.backend.root_helper,
|
||||||
|
mock.ANY,
|
||||||
|
self.backend.configuration.use_multipath_for_image_xfer,
|
||||||
|
self.backend.configuration.enforce_multipath_for_image_xfer)
|
||||||
|
|
||||||
|
mock_connect.assert_called_once_with(mock_conn_props.return_value)
|
||||||
|
mock_connect.return_value.attach.assert_not_called()
|
||||||
|
|
||||||
|
@mock.patch('cinderlib.objects.Volume.disconnect')
|
||||||
|
@mock.patch('os_brick.initiator.connector.get_connector_properties')
|
||||||
|
@mock.patch('cinderlib.objects.Volume.connect')
|
||||||
|
def test_attach_error_attach(self, mock_connect, mock_conn_props,
|
||||||
|
mock_disconnect):
|
||||||
|
vol = objects.Volume(self.backend_name, status='available', size=10)
|
||||||
|
mock_attach = mock_connect.return_value.attach
|
||||||
|
mock_attach.side_effect = exception.NotFound
|
||||||
|
|
||||||
|
self.assertRaises(exception.NotFound, vol.attach)
|
||||||
|
|
||||||
|
mock_conn_props.assert_called_once_with(
|
||||||
|
self.backend.root_helper,
|
||||||
|
mock.ANY,
|
||||||
|
self.backend.configuration.use_multipath_for_image_xfer,
|
||||||
|
self.backend.configuration.enforce_multipath_for_image_xfer)
|
||||||
|
|
||||||
|
mock_connect.assert_called_once_with(mock_conn_props.return_value)
|
||||||
|
mock_disconnect.assert_called_once_with(mock_connect.return_value)
|
||||||
|
|
||||||
|
def test_detach_not_local(self):
|
||||||
|
vol = objects.Volume(self.backend_name, status='available', size=10)
|
||||||
|
self.assertRaises(exception.NotLocal, vol.detach)
|
||||||
|
|
||||||
|
def test_detach(self):
|
||||||
|
vol = objects.Volume(self.backend_name, status='available', size=10)
|
||||||
|
mock_conn = mock.Mock()
|
||||||
|
vol.local_attach = mock_conn
|
||||||
|
|
||||||
|
vol.detach(mock.sentinel.force, mock.sentinel.ignore_errors)
|
||||||
|
mock_conn.detach.assert_called_once_with(mock.sentinel.force,
|
||||||
|
mock.sentinel.ignore_errors,
|
||||||
|
mock.ANY)
|
||||||
|
mock_conn.disconnect.assert_called_once_with(mock.sentinel.force)
|
||||||
|
|
||||||
|
def test_detach_error_detach(self):
|
||||||
|
vol = objects.Volume(self.backend_name, status='available', size=10)
|
||||||
|
mock_conn = mock.Mock()
|
||||||
|
mock_conn.detach.side_effect = exception.NotFound
|
||||||
|
vol.local_attach = mock_conn
|
||||||
|
|
||||||
|
self.assertRaises(exception.NotFound,
|
||||||
|
vol.detach,
|
||||||
|
False, mock.sentinel.ignore_errors)
|
||||||
|
|
||||||
|
mock_conn.detach.assert_called_once_with(False,
|
||||||
|
mock.sentinel.ignore_errors,
|
||||||
|
mock.ANY)
|
||||||
|
mock_conn.disconnect.assert_not_called()
|
||||||
|
|
||||||
|
def test_detach_error_disconnect(self):
|
||||||
|
vol = objects.Volume(self.backend_name, status='available', size=10)
|
||||||
|
mock_conn = mock.Mock()
|
||||||
|
mock_conn.disconnect.side_effect = exception.NotFound
|
||||||
|
vol.local_attach = mock_conn
|
||||||
|
|
||||||
|
self.assertRaises(objects.brick_exception.ExceptionChainer,
|
||||||
|
vol.detach,
|
||||||
|
mock.sentinel.force, False)
|
||||||
|
|
||||||
|
mock_conn.detach.assert_called_once_with(mock.sentinel.force,
|
||||||
|
False,
|
||||||
|
mock.ANY)
|
||||||
|
mock_conn.disconnect.assert_called_once_with(mock.sentinel.force)
|
||||||
|
|
||||||
|
@mock.patch('cinderlib.objects.Connection.connect')
|
||||||
|
def test_connect(self, mock_connect):
|
||||||
|
vol = objects.Volume(self.backend_name, status='available', size=10)
|
||||||
|
mock_connect.return_value._ovo = objects.cinder_objs.VolumeAttachment()
|
||||||
|
|
||||||
|
mock_export = self.backend.driver.create_export
|
||||||
|
mock_export.return_value = None
|
||||||
|
|
||||||
|
res = vol.connect(mock.sentinel.conn_dict)
|
||||||
|
|
||||||
|
mock_connect.assert_called_once_with(vol, mock.sentinel.conn_dict)
|
||||||
|
self.assertEqual([res], vol.connections)
|
||||||
|
self.assertEqual([res._ovo], vol._ovo.volume_attachment.objects)
|
||||||
|
self.assertEqual('in-use', vol.status)
|
||||||
|
self.persistence.set_volume.assert_called_once_with(vol)
|
||||||
|
|
||||||
|
@mock.patch('cinderlib.objects.Volume._remove_export')
|
||||||
|
@mock.patch('cinderlib.objects.Connection.connect')
|
||||||
|
def test_connect_error(self, mock_connect, mock_remove_export):
|
||||||
|
vol = objects.Volume(self.backend_name, status='available', size=10)
|
||||||
|
|
||||||
|
mock_export = self.backend.driver.create_export
|
||||||
|
mock_export.return_value = None
|
||||||
|
mock_connect.side_effect = exception.NotFound
|
||||||
|
|
||||||
|
self.assertRaises(exception.NotFound,
|
||||||
|
vol.connect, mock.sentinel.conn_dict)
|
||||||
|
|
||||||
|
mock_connect.assert_called_once_with(vol, mock.sentinel.conn_dict)
|
||||||
|
self.assertEqual('available', vol.status)
|
||||||
|
self.persistence.set_volume.assert_not_called()
|
||||||
|
mock_remove_export.assert_called_once_with()
|
||||||
|
|
||||||
|
@mock.patch('cinderlib.objects.Volume._disconnect')
|
||||||
|
def test_disconnect(self, mock_disconnect):
|
||||||
|
vol = objects.Volume(self.backend_name, status='available', size=10)
|
||||||
|
mock_conn = mock.Mock()
|
||||||
|
vol.disconnect(mock_conn, mock.sentinel.force)
|
||||||
|
mock_conn._disconnect.assert_called_once_with(mock.sentinel.force)
|
||||||
|
mock_disconnect.assert_called_once_with(mock_conn)
|
||||||
|
|
||||||
|
@mock.patch('cinderlib.objects.Volume._remove_export')
|
||||||
|
def test__disconnect(self, mock_remove_export):
|
||||||
|
vol = objects.Volume(self.backend_name, status='in-use', size=10)
|
||||||
|
|
||||||
|
vol._disconnect(mock.sentinel.connection)
|
||||||
|
|
||||||
|
mock_remove_export.assert_called_once_with()
|
||||||
|
self.assertEqual('available', vol.status)
|
||||||
|
self.persistence.set_volume.assert_called_once_with(vol)
|
||||||
|
|
||||||
|
def test__remove_export(self):
|
||||||
|
vol = objects.Volume(self.backend_name, status='in-use', size=10)
|
||||||
|
|
||||||
|
vol._remove_export()
|
||||||
|
|
||||||
|
self.backend.driver.remove_export.assert_called_once_with(vol._context,
|
||||||
|
vol._ovo)
|
||||||
|
|
||||||
|
@mock.patch('cinderlib.objects.Volume._remove_export')
|
||||||
|
def test_cleanup(self, mock_remove_export):
|
||||||
|
vol = objects.Volume(self.backend_name, status='in-use', size=10)
|
||||||
|
connections = [mock.Mock(), mock.Mock()]
|
||||||
|
vol._connections = connections
|
||||||
|
|
||||||
|
vol.cleanup()
|
||||||
|
|
||||||
|
mock_remove_export.assert_called_once_with()
|
||||||
|
for c in connections:
|
||||||
|
c.detach.asssert_called_once_with()
|
||||||
0
cinderlib/tests/unit/persistence/__init__.py
Normal file
0
cinderlib/tests/unit/persistence/__init__.py
Normal file
@@ -13,45 +13,39 @@
|
|||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
import unittest2
|
|
||||||
|
|
||||||
from cinder.cmd import volume as volume_cmd
|
from cinder.cmd import volume as volume_cmd
|
||||||
|
from cinder.db.sqlalchemy import api
|
||||||
from cinder.db.sqlalchemy import models
|
from cinder.db.sqlalchemy import models
|
||||||
from oslo_versionedobjects import fields
|
from oslo_versionedobjects import fields
|
||||||
|
|
||||||
import cinderlib
|
import cinderlib
|
||||||
from tests.unit import utils
|
from cinderlib.tests.unit import base
|
||||||
|
from cinderlib.tests.unit import utils
|
||||||
|
|
||||||
|
|
||||||
class BasePersistenceTest(unittest2.TestCase):
|
class BasePersistenceTest(base.BaseTest):
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpClass(cls):
|
def setUpClass(cls):
|
||||||
cls.original_impl = volume_cmd.session.IMPL
|
cls.original_impl = volume_cmd.session.IMPL
|
||||||
# We check the entrypoint is working
|
cinderlib.Backend.global_initialization = False
|
||||||
cinderlib.setup(persistence_config=cls.PERSISTENCE_CFG)
|
cinderlib.setup(persistence_config=cls.PERSISTENCE_CFG)
|
||||||
cls.persistence = cinderlib.Backend.persistence
|
|
||||||
cls.context = cinderlib.objects.CONTEXT
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def tearDownClass(cls):
|
def tearDownClass(cls):
|
||||||
volume_cmd.session.IMPL = cls.original_impl
|
volume_cmd.session.IMPL = cls.original_impl
|
||||||
cinderlib.Backend.global_initialization = False
|
cinderlib.Backend.global_initialization = False
|
||||||
|
api.main_context_manager = api.enginefacade.transaction_context()
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.backend = utils.FakeBackend()
|
super(BasePersistenceTest, self).setUp()
|
||||||
|
self.context = cinderlib.objects.CONTEXT
|
||||||
def tearDown(self):
|
|
||||||
# Clear all existing backends
|
|
||||||
cinderlib.Backend.backends = {}
|
|
||||||
super(BasePersistenceTest, self).tearDown()
|
|
||||||
|
|
||||||
def sorted(self, resources, key='id'):
|
def sorted(self, resources, key='id'):
|
||||||
return sorted(resources, key=lambda x: getattr(x, key))
|
return sorted(resources, key=lambda x: getattr(x, key))
|
||||||
|
|
||||||
def create_n_volumes(self, n):
|
def create_n_volumes(self, n):
|
||||||
return self.create_volumes([{'size': i, 'name': 'disk%s' % i}
|
return self.create_volumes([{'size': i, 'name': 'disk%s' % i}
|
||||||
for i in range(1, n+1)])
|
for i in range(1, n + 1)])
|
||||||
|
|
||||||
def create_volumes(self, data, sort=True):
|
def create_volumes(self, data, sort=True):
|
||||||
vols = []
|
vols = []
|
||||||
@@ -21,7 +21,7 @@ from oslo_db import api as oslo_db_api
|
|||||||
|
|
||||||
import cinderlib
|
import cinderlib
|
||||||
from cinderlib.persistence import dbms
|
from cinderlib.persistence import dbms
|
||||||
from tests.unit.persistence import base
|
from cinderlib.tests.unit.persistence import base
|
||||||
|
|
||||||
|
|
||||||
class TestDBPersistence(base.BasePersistenceTest):
|
class TestDBPersistence(base.BasePersistenceTest):
|
||||||
@@ -105,6 +105,5 @@ class TestDBPersistence(base.BasePersistenceTest):
|
|||||||
self.assertListEqualObj(expected, actual)
|
self.assertListEqualObj(expected, actual)
|
||||||
|
|
||||||
|
|
||||||
# TODO: Figure out why we can't run both DB persistence test classes
|
class TestMemoryDBPersistence(TestDBPersistence):
|
||||||
# class TestMemoryDBPersistence(TestDBPersistence):
|
PERSISTENCE_CFG = {'storage': 'memory_db'}
|
||||||
# PERSISTENCE_CFG = {'storage': 'memory_db'}
|
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
import cinderlib
|
import cinderlib
|
||||||
from tests.unit.persistence import base
|
from cinderlib.tests.unit.persistence import base
|
||||||
|
|
||||||
|
|
||||||
class TestMemoryPersistence(base.BasePersistenceTest):
|
class TestMemoryPersistence(base.BasePersistenceTest):
|
||||||
241
cinderlib/tests/unit/test_cinderlib.py
Normal file
241
cinderlib/tests/unit/test_cinderlib.py
Normal file
@@ -0,0 +1,241 @@
|
|||||||
|
# Copyright (c) 2017, Red Hat, Inc.
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
import mock
|
||||||
|
from oslo_config import cfg
|
||||||
|
|
||||||
|
import cinderlib
|
||||||
|
from cinderlib import objects
|
||||||
|
from cinderlib.tests.unit import base
|
||||||
|
|
||||||
|
|
||||||
|
class TestCinderlib(base.BaseTest):
|
||||||
|
|
||||||
|
def test_lib_assignations(self):
|
||||||
|
self.assertEqual(cinderlib.setup, cinderlib.Backend.global_setup)
|
||||||
|
self.assertEqual(cinderlib.Backend, cinderlib.objects.Backend)
|
||||||
|
self.assertEqual(cinderlib.Backend,
|
||||||
|
cinderlib.objects.Object.backend_class)
|
||||||
|
|
||||||
|
@mock.patch('oslo_utils.importutils.import_object')
|
||||||
|
@mock.patch('cinderlib.Backend._set_backend_config')
|
||||||
|
@mock.patch('cinderlib.Backend.global_setup')
|
||||||
|
def test_init(self, mock_global_setup, mock_config, mock_import):
|
||||||
|
cfg.CONF.set_override('host', 'host')
|
||||||
|
driver_cfg = {'k': 'v', 'k2': 'v2', 'volume_backend_name': 'Test'}
|
||||||
|
cinderlib.Backend.global_initialization = False
|
||||||
|
driver = mock_import.return_value
|
||||||
|
driver.get_volume_stats.return_value = {
|
||||||
|
'pools': [{'pool_name': 'default'}]}
|
||||||
|
|
||||||
|
backend = objects.Backend(**driver_cfg)
|
||||||
|
|
||||||
|
mock_global_setup.assert_called_once_with()
|
||||||
|
self.assertIn('Test', objects.Backend.backends)
|
||||||
|
self.assertEqual(backend, objects.Backend.backends['Test'])
|
||||||
|
mock_config.assert_called_once_with(driver_cfg)
|
||||||
|
|
||||||
|
conf = mock_config.return_value
|
||||||
|
mock_import.assert_called_once_with(conf.volume_driver,
|
||||||
|
configuration=conf,
|
||||||
|
db=self.persistence.db,
|
||||||
|
host='host@Test',
|
||||||
|
cluster_name=None,
|
||||||
|
active_backend_id=None)
|
||||||
|
self.assertEqual(backend.driver, driver)
|
||||||
|
driver.do_setup.assert_called_once_with(objects.CONTEXT)
|
||||||
|
driver.check_for_setup_error.assert_called_once_with()
|
||||||
|
driver.init_capabilities.assert_called_once_with()
|
||||||
|
driver.set_throttle.assert_called_once_with()
|
||||||
|
driver.set_initialized.assert_called_once_with()
|
||||||
|
self.assertEqual(driver_cfg, backend._driver_cfg)
|
||||||
|
self.assertIsNone(backend._volumes)
|
||||||
|
driver.get_volume_stats.assert_called_once_with(refresh=False)
|
||||||
|
self.assertEqual(('default',), backend.pool_names)
|
||||||
|
|
||||||
|
@mock.patch('urllib3.disable_warnings')
|
||||||
|
@mock.patch('cinder.coordination.COORDINATOR')
|
||||||
|
@mock.patch('cinderlib.Backend._set_priv_helper')
|
||||||
|
@mock.patch('cinderlib.Backend._set_logging')
|
||||||
|
@mock.patch('cinderlib.cinderlib.serialization')
|
||||||
|
@mock.patch('cinderlib.Backend.set_persistence')
|
||||||
|
def test_global_setup(self, mock_set_pers, mock_serial, mock_log,
|
||||||
|
mock_sudo, mock_coord, mock_disable_warn):
|
||||||
|
cls = objects.Backend
|
||||||
|
cls.global_initialization = False
|
||||||
|
cinder_cfg = {'k': 'v', 'k2': 'v2'}
|
||||||
|
|
||||||
|
cls.global_setup('file_locks',
|
||||||
|
mock.sentinel.root_helper,
|
||||||
|
mock.sentinel.ssl_warnings,
|
||||||
|
mock.sentinel.disable_logs,
|
||||||
|
mock.sentinel.non_uuid_ids,
|
||||||
|
mock.sentinel.backend_info,
|
||||||
|
mock.sentinel.project_id,
|
||||||
|
mock.sentinel.user_id,
|
||||||
|
mock.sentinel.pers_cfg,
|
||||||
|
mock.sentinel.fail_missing_backend,
|
||||||
|
'mock.sentinel.host',
|
||||||
|
**cinder_cfg)
|
||||||
|
|
||||||
|
self.assertEqual('file_locks', cfg.CONF.oslo_concurrency.lock_path)
|
||||||
|
self.assertEqual('file://file_locks',
|
||||||
|
cfg.CONF.coordination.backend_url)
|
||||||
|
self.assertEqual(mock.sentinel.fail_missing_backend,
|
||||||
|
cls.fail_on_missing_backend)
|
||||||
|
self.assertEqual(mock.sentinel.root_helper, cls.root_helper)
|
||||||
|
self.assertEqual(mock.sentinel.project_id, cls.project_id)
|
||||||
|
self.assertEqual(mock.sentinel.user_id, cls.user_id)
|
||||||
|
self.assertEqual(mock.sentinel.non_uuid_ids, cls.non_uuid_ids)
|
||||||
|
self.assertEqual('mock.sentinel.host', cfg.CONF.host)
|
||||||
|
mock_set_pers.assert_called_once_with(mock.sentinel.pers_cfg)
|
||||||
|
|
||||||
|
self.assertEqual(cinderlib.__version__, cfg.CONF.version)
|
||||||
|
|
||||||
|
mock_serial.setup.assert_called_once_with(cls)
|
||||||
|
mock_log.assert_called_once_with(mock.sentinel.disable_logs)
|
||||||
|
mock_sudo.assert_called_once_with(mock.sentinel.root_helper)
|
||||||
|
mock_coord.start.assert_called_once_with()
|
||||||
|
|
||||||
|
self.assertEqual(2, mock_disable_warn.call_count)
|
||||||
|
self.assertTrue(cls.global_initialization)
|
||||||
|
self.assertEqual(mock.sentinel.backend_info,
|
||||||
|
cls.output_all_backend_info)
|
||||||
|
|
||||||
|
def test_pool_names(self):
|
||||||
|
pool_names = [mock.sentinel._pool_names]
|
||||||
|
self.backend._pool_names = pool_names
|
||||||
|
self.assertEqual(pool_names, self.backend.pool_names)
|
||||||
|
|
||||||
|
def test_volumes(self):
|
||||||
|
self.backend._volumes = None
|
||||||
|
res = self.backend.volumes
|
||||||
|
self.assertEqual(self.persistence.get_volumes.return_value, res)
|
||||||
|
self.assertEqual(self.persistence.get_volumes.return_value,
|
||||||
|
self.backend._volumes)
|
||||||
|
self.persistence.get_volumes.assert_called_once_with(
|
||||||
|
backend_name=self.backend.id)
|
||||||
|
|
||||||
|
def test_id(self):
|
||||||
|
self.assertEqual(self.backend._driver_cfg['volume_backend_name'],
|
||||||
|
self.backend.id)
|
||||||
|
|
||||||
|
def test_volumes_filtered(self):
|
||||||
|
res = self.backend.volumes_filtered(mock.sentinel.vol_id,
|
||||||
|
mock.sentinel.vol_name)
|
||||||
|
self.assertEqual(self.persistence.get_volumes.return_value, res)
|
||||||
|
self.assertEqual([], self.backend._volumes)
|
||||||
|
self.persistence.get_volumes.assert_called_once_with(
|
||||||
|
backend_name=self.backend.id,
|
||||||
|
volume_id=mock.sentinel.vol_id,
|
||||||
|
volume_name=mock.sentinel.vol_name)
|
||||||
|
|
||||||
|
def test_stats(self):
|
||||||
|
expect = {'pools': [mock.sentinel.data]}
|
||||||
|
with mock.patch.object(self.backend.driver, 'get_volume_stats',
|
||||||
|
return_value=expect) as mock_stat:
|
||||||
|
res = self.backend.stats(mock.sentinel.refresh)
|
||||||
|
self.assertEqual(expect, res)
|
||||||
|
mock_stat.assert_called_once_with(refresh=mock.sentinel.refresh)
|
||||||
|
|
||||||
|
def test_stats_single(self):
|
||||||
|
stat_value = {'driver_version': 'v1', 'key': 'value'}
|
||||||
|
expect = {'driver_version': 'v1', 'key': 'value',
|
||||||
|
'pools': [{'key': 'value', 'pool_name': 'fake_backend'}]}
|
||||||
|
with mock.patch.object(self.backend.driver, 'get_volume_stats',
|
||||||
|
return_value=stat_value) as mock_stat:
|
||||||
|
res = self.backend.stats(mock.sentinel.refresh)
|
||||||
|
self.assertEqual(expect, res)
|
||||||
|
mock_stat.assert_called_once_with(refresh=mock.sentinel.refresh)
|
||||||
|
|
||||||
|
@mock.patch('cinderlib.objects.Volume')
|
||||||
|
def test_create_volume(self, mock_vol):
|
||||||
|
kwargs = {'k': 'v', 'k2': 'v2'}
|
||||||
|
res = self.backend.create_volume(mock.sentinel.size,
|
||||||
|
mock.sentinel.name,
|
||||||
|
mock.sentinel.desc,
|
||||||
|
mock.sentinel.boot,
|
||||||
|
**kwargs)
|
||||||
|
self.assertEqual(mock_vol.return_value, res)
|
||||||
|
mock_vol.assert_called_once_with(self.backend, size=mock.sentinel.size,
|
||||||
|
name=mock.sentinel.name,
|
||||||
|
description=mock.sentinel.desc,
|
||||||
|
bootable=mock.sentinel.boot,
|
||||||
|
**kwargs)
|
||||||
|
mock_vol.return_value.create.assert_called_once_with()
|
||||||
|
|
||||||
|
def test__volume_removed_no_list(self):
|
||||||
|
self.backend._volume_removed(mock.sentinel.volume)
|
||||||
|
|
||||||
|
def test__volume_removed(self):
|
||||||
|
vol = cinderlib.objects.Volume(self.backend, size=10)
|
||||||
|
vol2 = cinderlib.objects.Volume(self.backend, id=vol.id, size=10)
|
||||||
|
self.backend._volumes.append(vol)
|
||||||
|
self.backend._volume_removed(vol2)
|
||||||
|
self.assertEqual([], self.backend.volumes)
|
||||||
|
|
||||||
|
def test__volume_created(self):
|
||||||
|
vol = cinderlib.objects.Volume(self.backend, size=10)
|
||||||
|
self.backend._volume_created(vol)
|
||||||
|
self.assertEqual([vol], self.backend.volumes)
|
||||||
|
|
||||||
|
def test__volume_created_is_none(self):
|
||||||
|
vol = cinderlib.objects.Volume(self.backend, size=10)
|
||||||
|
self.backend._volume_created(vol)
|
||||||
|
self.assertEqual([vol], self.backend.volumes)
|
||||||
|
|
||||||
|
def test_validate_connector(self):
|
||||||
|
self.backend.validate_connector(mock.sentinel.connector)
|
||||||
|
self.backend.driver.validate_connector.assert_called_once_with(
|
||||||
|
mock.sentinel.connector)
|
||||||
|
|
||||||
|
@mock.patch('cinderlib.objects.setup')
|
||||||
|
@mock.patch('cinderlib.persistence.setup')
|
||||||
|
def test_set_persistence(self, mock_pers_setup, mock_obj_setup):
|
||||||
|
cinderlib.Backend.global_initialization = True
|
||||||
|
|
||||||
|
cinderlib.Backend.set_persistence(mock.sentinel.pers_cfg)
|
||||||
|
|
||||||
|
mock_pers_setup.assert_called_once_with(mock.sentinel.pers_cfg)
|
||||||
|
self.assertEqual(mock_pers_setup.return_value,
|
||||||
|
cinderlib.Backend.persistence)
|
||||||
|
mock_obj_setup.assert_called_once_with(mock_pers_setup.return_value,
|
||||||
|
cinderlib.Backend,
|
||||||
|
self.backend.project_id,
|
||||||
|
self.backend.user_id,
|
||||||
|
self.backend.non_uuid_ids)
|
||||||
|
self.assertEqual(mock_pers_setup.return_value.db,
|
||||||
|
self.backend.driver.db)
|
||||||
|
|
||||||
|
def test_config(self):
|
||||||
|
self.backend.output_all_backend_info = False
|
||||||
|
res = self.backend.config
|
||||||
|
self.assertEqual({'volume_backend_name': self.backend.id}, res)
|
||||||
|
|
||||||
|
def test_config_full(self):
|
||||||
|
self.backend.output_all_backend_info = True
|
||||||
|
with mock.patch.object(self.backend, '_driver_cfg') as mock_driver:
|
||||||
|
res = self.backend.config
|
||||||
|
self.assertEqual(mock_driver, res)
|
||||||
|
|
||||||
|
def test_refresh(self):
|
||||||
|
self.backend.refresh()
|
||||||
|
self.persistence.get_volumes.assert_called_once_with(
|
||||||
|
backend_name=self.backend.id)
|
||||||
|
|
||||||
|
def test_refresh_no_call(self):
|
||||||
|
self.backend._volumes = None
|
||||||
|
self.backend.refresh()
|
||||||
|
self.persistence.get_volumes.assert_not_called()
|
||||||
@@ -16,6 +16,11 @@
|
|||||||
import mock
|
import mock
|
||||||
|
|
||||||
import cinderlib
|
import cinderlib
|
||||||
|
from cinderlib.persistence import base
|
||||||
|
|
||||||
|
|
||||||
|
def get_mock_persistence():
|
||||||
|
return mock.MagicMock(spec=base.PersistenceDriverBase)
|
||||||
|
|
||||||
|
|
||||||
class FakeBackend(cinderlib.Backend):
|
class FakeBackend(cinderlib.Backend):
|
||||||
@@ -24,4 +29,6 @@ class FakeBackend(cinderlib.Backend):
|
|||||||
cinderlib.Backend.backends[driver_name] = self
|
cinderlib.Backend.backends[driver_name] = self
|
||||||
self._driver_cfg = {'volume_backend_name': driver_name}
|
self._driver_cfg = {'volume_backend_name': driver_name}
|
||||||
self.driver = mock.Mock()
|
self.driver = mock.Mock()
|
||||||
|
self.driver.persistence = cinderlib.Backend.persistence
|
||||||
self._pool_names = (driver_name,)
|
self._pool_names = (driver_name,)
|
||||||
|
self._volumes = []
|
||||||
@@ -18,7 +18,7 @@ import six
|
|||||||
if six.PY2:
|
if six.PY2:
|
||||||
# Python 2 workaround for getaddrinfo (fails if port is valid unicode)
|
# Python 2 workaround for getaddrinfo (fails if port is valid unicode)
|
||||||
def my_getaddrinfo(original, host, port, *args, **kwargs):
|
def my_getaddrinfo(original, host, port, *args, **kwargs):
|
||||||
if isinstance(port, unicode):
|
if isinstance(port, six.text_type):
|
||||||
port = str(port)
|
port = str(port)
|
||||||
return original(host, port, *args, **kwargs)
|
return original(host, port, *args, **kwargs)
|
||||||
import functools
|
import functools
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
ddt>=1.0.1
|
||||||
unittest2
|
unittest2
|
||||||
pyyaml
|
pyyaml
|
||||||
pip==8.1.2
|
pip==8.1.2
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
Sphinx==1.6.5
|
Sphinx==1.6.5
|
||||||
git+https://github.com/akrog/modulefaker.git#egg=modulefaker
|
git+https://github.com/akrog/modulefaker.git#egg=modulefaker
|
||||||
git+https://github.com/akrog/cindermock.git
|
git+https://github.com/akrog/cindermock.git
|
||||||
git+https://github.com/akrog/nosbrickmock.git
|
|
||||||
|
|||||||
3
setup.py
3
setup.py
@@ -17,7 +17,6 @@ with open('HISTORY.rst') as history_file:
|
|||||||
|
|
||||||
requirements = [
|
requirements = [
|
||||||
'cinder>=11.0',
|
'cinder>=11.0',
|
||||||
'nos-brick',
|
|
||||||
]
|
]
|
||||||
|
|
||||||
test_requirements = [
|
test_requirements = [
|
||||||
@@ -63,7 +62,7 @@ setuptools.setup(
|
|||||||
author="Gorka Eguileor",
|
author="Gorka Eguileor",
|
||||||
author_email='geguileo@redhat.com',
|
author_email='geguileo@redhat.com',
|
||||||
url='https://github.com/akrog/cinderlib',
|
url='https://github.com/akrog/cinderlib',
|
||||||
packages=setuptools.find_packages(exclude=['tmp', 'tests*']),
|
packages=setuptools.find_packages(exclude=['tmp', 'cinderlib/tests']),
|
||||||
include_package_data=False,
|
include_package_data=False,
|
||||||
install_requires=requirements,
|
install_requires=requirements,
|
||||||
extras_require=extras,
|
extras_require=extras,
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
"""
|
|
||||||
test_cinderlib
|
|
||||||
----------------------------------
|
|
||||||
|
|
||||||
Tests for `cinderlib` module.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import unittest2
|
|
||||||
|
|
||||||
import cinderlib
|
|
||||||
|
|
||||||
|
|
||||||
class TestCinderlib(unittest2.TestCase):
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def tearDown(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def test_lib_setup(self):
|
|
||||||
self.assertEqual(cinderlib.setup, cinderlib.Backend.global_setup)
|
|
||||||
19
tools/lvm.yaml
Normal file
19
tools/lvm.yaml
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# For Fedora, CentOS, RHEL we require the targetcli package.
|
||||||
|
# For Ubuntu we require lio-utils or changing the target iscsi_helper
|
||||||
|
#
|
||||||
|
|
||||||
|
# Logs are way too verbose, so we disable them
|
||||||
|
logs: false
|
||||||
|
|
||||||
|
# LVM backend uses cinder-rtstool command that is installed by Cinder in the
|
||||||
|
# virtual environment, so we need the custom sudo command that inherits the
|
||||||
|
# virtualenv binaries PATH
|
||||||
|
venv_sudo: false
|
||||||
|
|
||||||
|
# We only define one backend
|
||||||
|
backends:
|
||||||
|
- volume_backend_name: lvm
|
||||||
|
volume_driver: cinder.volume.drivers.lvm.LVMVolumeDriver
|
||||||
|
volume_group: cinder-volumes
|
||||||
|
target_protocol: iscsi
|
||||||
|
target_helper: lioadm
|
||||||
6
tox.ini
6
tox.ini
@@ -18,7 +18,7 @@ setenv =
|
|||||||
deps= -r{toxinidir}/requirements_dev.txt
|
deps= -r{toxinidir}/requirements_dev.txt
|
||||||
|
|
||||||
commands =
|
commands =
|
||||||
unit2 discover -v -s tests/unit []
|
unit2 discover -v -s cinderlib/tests/unit []
|
||||||
|
|
||||||
[testenv:functional]
|
[testenv:functional]
|
||||||
sitepackages = True
|
sitepackages = True
|
||||||
@@ -28,6 +28,6 @@ basepython=python2.7
|
|||||||
envdir = {toxworkdir}/py27
|
envdir = {toxworkdir}/py27
|
||||||
|
|
||||||
# Pass on the location of the backend configuration to the tests
|
# Pass on the location of the backend configuration to the tests
|
||||||
setenv = CL_FTEST_CFG = {env:CL_FTEST_CFG:tests/functional/lvm.yaml}
|
setenv = CL_FTEST_CFG = {env:CL_FTEST_CFG:tools/lvm.yaml}
|
||||||
commands =
|
commands =
|
||||||
unit2 discover -v -s tests/functional []
|
unit2 discover -v -s cinderlib/tests/functional []
|
||||||
|
|||||||
Reference in New Issue
Block a user