Merge "Add privsep support"
This commit is contained in:
commit
4f4c8c15f5
|
@ -76,7 +76,6 @@
|
|||
vars:
|
||||
tox_environment:
|
||||
CL_FTEST_CFG: "{{ ansible_user_dir }}/{{ zuul.projects['opendev.org/openstack/cinderlib'].src_dir }}/cinderlib/tests/functional/ceph.yaml"
|
||||
CL_FTEST_ROOT_HELPER: sudo
|
||||
# These come from great-great-grandparent tox job
|
||||
NOSE_WITH_HTML_OUTPUT: 1
|
||||
NOSE_HTML_OUT_FILE: nose_results.html
|
||||
|
|
|
@ -20,6 +20,8 @@ except ImportError:
|
|||
# For everyone else
|
||||
import importlib_metadata
|
||||
|
||||
from os_brick.initiator import connector
|
||||
|
||||
from cinderlib import _fake_packages # noqa F401
|
||||
from cinderlib import cinderlib
|
||||
from cinderlib import objects
|
||||
|
@ -48,5 +50,5 @@ setup = cinderlib.setup
|
|||
Backend = cinderlib.Backend
|
||||
|
||||
# This gets reassigned on initialization by nos_brick.init
|
||||
get_connector_properties = objects.brick_connector.get_connector_properties
|
||||
get_connector_properties = connector.get_connector_properties
|
||||
list_supported_drivers = cinderlib.Backend.list_supported_drivers
|
||||
|
|
|
@ -12,10 +12,13 @@
|
|||
# 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 configparser
|
||||
import glob
|
||||
import json as json_lib
|
||||
import logging
|
||||
import multiprocessing
|
||||
import os
|
||||
import shutil
|
||||
|
||||
from cinder import coordination
|
||||
from cinder.db import api as db_api
|
||||
|
@ -26,16 +29,18 @@ from cinder import objects as cinder_objects
|
|||
cinder_objects.register_all() # noqa
|
||||
|
||||
from cinder.interface import util as cinder_interface_util
|
||||
import cinder.privsep
|
||||
from cinder import utils
|
||||
from cinder.volume import configuration
|
||||
from cinder.volume import manager # noqa We need to import config options
|
||||
import os_brick.privileged
|
||||
from oslo_config import cfg
|
||||
from oslo_log import log as oslo_logging
|
||||
from oslo_privsep import priv_context
|
||||
from oslo_utils import importutils
|
||||
import urllib3
|
||||
|
||||
import cinderlib
|
||||
from cinderlib import nos_brick
|
||||
from cinderlib import objects
|
||||
from cinderlib import persistence
|
||||
from cinderlib import serialization
|
||||
|
@ -295,8 +300,8 @@ class Backend(object):
|
|||
if cls.global_initialization:
|
||||
raise Exception('Already setup')
|
||||
|
||||
cls.im_root = os.getuid() == 0
|
||||
cls.fail_on_missing_backend = fail_on_missing_backend
|
||||
cls.root_helper = root_helper
|
||||
cls.project_id = project_id
|
||||
cls.user_id = user_id
|
||||
cls.non_uuid_ids = non_uuid_ids
|
||||
|
@ -338,8 +343,74 @@ class Backend(object):
|
|||
|
||||
@classmethod
|
||||
def _set_priv_helper(cls, root_helper):
|
||||
utils.get_root_helper = lambda: root_helper
|
||||
nos_brick.init(root_helper)
|
||||
# If we are using a virtual environment then the rootwrap config files
|
||||
# Should be within the environment and not under /etc/cinder/
|
||||
venv = os.environ.get('VIRTUAL_ENV')
|
||||
if (venv and not cfg.CONF.rootwrap_config.startswith(venv) and
|
||||
not os.path.exists(cfg.CONF.rootwrap_config)):
|
||||
|
||||
# We need to remove the absolute path (initial '/') to generate the
|
||||
# config path under the virtualenv
|
||||
# for the join to work.
|
||||
wrap_path = cfg.CONF.rootwrap_config[1:]
|
||||
venv_wrap_file = os.path.join(venv, wrap_path)
|
||||
venv_wrap_dir = os.path.dirname(venv_wrap_file)
|
||||
|
||||
# In virtual environments our rootwrap config file is no longer
|
||||
# '/etc/cinder/rootwrap.conf'. We have 2 possible roots, it's
|
||||
# either the virtualenv's directory or our where our sources are if
|
||||
# we have installed cinder as editable.
|
||||
|
||||
# For editable we need to copy the files into the virtualenv if we
|
||||
# haven't copied them before.
|
||||
if not utils.__file__.startswith(venv):
|
||||
# If we haven't copied the files yet
|
||||
if not os.path.exists(venv_wrap_file):
|
||||
editable_link = glob.glob(os.path.join(
|
||||
venv, 'lib/python*/site-packages/cinder.egg-link'))
|
||||
with open(editable_link[0], 'r') as f:
|
||||
cinder_source_path = f.read().split('\n')[0]
|
||||
cinder_source_etc = os.path.join(cinder_source_path,
|
||||
'etc/cinder')
|
||||
|
||||
shutil.copytree(cinder_source_etc, venv_wrap_dir)
|
||||
|
||||
# For venvs we need to update configured filters_path and exec_dirs
|
||||
parser = configparser.ConfigParser()
|
||||
parser.read(venv_wrap_file)
|
||||
# Change contents if we haven't done it already
|
||||
if not parser['DEFAULT']['filters_path'].startswith(venv_wrap_dir):
|
||||
parser['DEFAULT']['filters_path'] = os.path.join(venv_wrap_dir,
|
||||
'rootwrap.d')
|
||||
parser['DEFAULT']['exec_dirs'] = (
|
||||
os.path.join(venv, 'bin,') +
|
||||
parser['DEFAULT']['exec_dirs'])
|
||||
|
||||
with open(venv_wrap_file, 'w') as f:
|
||||
parser.write(f)
|
||||
|
||||
# Don't use set_override because it doesn't work as it should
|
||||
cfg.CONF.rootwrap_config = venv_wrap_file
|
||||
|
||||
# The default Cinder roothelper in Cinder and privsep is sudo, so
|
||||
# nothing to do in those cases.
|
||||
if root_helper != 'sudo':
|
||||
# Get the current helper (usually 'sudo cinder-rootwrap
|
||||
# <CONF.rootwrap_config>') and replace the sudo part
|
||||
original_helper = utils.get_root_helper()
|
||||
|
||||
# If we haven't already set the helper
|
||||
if root_helper not in original_helper:
|
||||
new_helper = original_helper.replace('sudo', root_helper)
|
||||
utils.get_root_helper = lambda: new_helper
|
||||
|
||||
# Initialize privsep's context to not use 'sudo'
|
||||
priv_context.init(root_helper=[root_helper])
|
||||
|
||||
# Don't use server/client mode when running as root
|
||||
client_mode = not cls.im_root
|
||||
cinder.privsep.sys_admin_pctxt.set_client_mode(client_mode)
|
||||
os_brick.privileged.default.set_client_mode(client_mode)
|
||||
|
||||
@property
|
||||
def config(self):
|
||||
|
|
|
@ -1,311 +0,0 @@
|
|||
# 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 errno
|
||||
import functools
|
||||
import os
|
||||
import traceback
|
||||
|
||||
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_log import log as logging
|
||||
from oslo_privsep import priv_context
|
||||
from oslo_utils import fileutils
|
||||
from oslo_utils import strutils
|
||||
import six
|
||||
|
||||
import cinderlib
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
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.
|
||||
self._setup_rbd_class()
|
||||
|
||||
# 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)
|
||||
|
||||
link_name = self.get_rbd_device_name(pool, volume)
|
||||
real_path = os.path.realpath(link_name)
|
||||
|
||||
try:
|
||||
# Map RBD volume if it's not already mapped
|
||||
if not os.path.islink(link_name) or not os.path.exists(real_path):
|
||||
cmd = ['rbd', 'map', volume, '--pool', pool, '--conf', conf]
|
||||
cmd += self._get_rbd_args(connection_properties)
|
||||
stdout, stderr = self._execute(*cmd,
|
||||
root_helper=self._root_helper,
|
||||
run_as_root=True)
|
||||
real_path = stdout.strip()
|
||||
# The host may not have RBD installed, and therefore won't
|
||||
# create the symlinks, ensure they exist
|
||||
if self.containerized:
|
||||
self._ensure_link(real_path, link_name)
|
||||
except Exception as exec_exception:
|
||||
try:
|
||||
try:
|
||||
self._unmap(real_path, conf, connection_properties)
|
||||
finally:
|
||||
fileutils.delete_if_exists(conf)
|
||||
except Exception:
|
||||
exc = traceback.format_exc()
|
||||
LOG.error('Exception occurred while cleaning up after '
|
||||
'connection error\n%s', exc)
|
||||
finally:
|
||||
raise exception.BrickException('Error connecting volume: %s' %
|
||||
six.text_type(exec_exception))
|
||||
|
||||
return {'path': real_path,
|
||||
'conf': conf,
|
||||
'type': 'block'}
|
||||
|
||||
def _ensure_link(self, source, link_name):
|
||||
self._ensure_dir(os.path.dirname(link_name))
|
||||
if self.im_root:
|
||||
# If the link exists, remove it in case it's a leftover
|
||||
if os.path.exists(link_name):
|
||||
os.remove(link_name)
|
||||
try:
|
||||
os.symlink(source, link_name)
|
||||
except OSError as exc:
|
||||
# Don't fail if symlink creation fails because it exists.
|
||||
# It means that ceph-common has just created it.
|
||||
if exc.errno != errno.EEXIST:
|
||||
raise
|
||||
else:
|
||||
self._execute('ln', '-s', '-f', source, link_name,
|
||||
root_helper=self._root_helper, run_as_root=True)
|
||||
|
||||
def check_valid_device(self, path, run_as_root=True):
|
||||
"""Verify an existing RBD handle is connected and valid."""
|
||||
if self.im_root:
|
||||
try:
|
||||
with open(path, 'rb') as f:
|
||||
f.read(4096)
|
||||
except Exception:
|
||||
return False
|
||||
return True
|
||||
|
||||
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 _get_vol_data(self, connection_properties):
|
||||
self._setup_rbd_class()
|
||||
pool, volume = connection_properties['name'].split('/')
|
||||
link_name = self.get_rbd_device_name(pool, volume)
|
||||
real_dev_path = os.path.realpath(link_name)
|
||||
return link_name, real_dev_path
|
||||
|
||||
def _unmap(self, real_dev_path, conf_file, connection_properties):
|
||||
if os.path.exists(real_dev_path):
|
||||
cmd = ['rbd', 'unmap', real_dev_path, '--conf', conf_file]
|
||||
cmd += self._get_rbd_args(connection_properties)
|
||||
self._execute(*cmd, root_helper=self._root_helper,
|
||||
run_as_root=True)
|
||||
|
||||
def disconnect_volume(self, connection_properties, device_info,
|
||||
force=False, ignore_errors=False):
|
||||
conf_file = device_info['conf']
|
||||
link_name, real_dev_path = self._get_vol_data(connection_properties)
|
||||
|
||||
self._unmap(real_dev_path, conf_file, connection_properties)
|
||||
if self.containerized:
|
||||
unlink_root(link_name)
|
||||
fileutils.delete_if_exists(conf_file)
|
||||
|
||||
def _ensure_dir(self, path):
|
||||
if self.im_root:
|
||||
try:
|
||||
os.makedirs(path, 0o755)
|
||||
except OSError as exc:
|
||||
# Don't fail if directory already exists, as our job is done.
|
||||
if exc.errno != errno.EEXIST:
|
||||
raise
|
||||
else:
|
||||
self._execute('mkdir', '-p', '-m0755', path,
|
||||
root_helper=self._root_helper, run_as_root=True)
|
||||
|
||||
@staticmethod
|
||||
def _in_container():
|
||||
if os.stat('/proc').st_dev <= 4:
|
||||
return False
|
||||
|
||||
# When running containerized / is /var/lib/docker when running in
|
||||
# Docker /var/lib/containers when running in Podman, and /var/lib/lxc
|
||||
# when in LXC
|
||||
with open('/proc/1/mounts', 'r') as f:
|
||||
for line in f.readlines():
|
||||
data = line.split(' ', 2)
|
||||
if data[1] == '/':
|
||||
return '/var/lib/' in data[2]
|
||||
# Just in case, say we are
|
||||
LOG.warning("Couldn't detect if running on container, assuming we are")
|
||||
return True
|
||||
|
||||
def _setup_class(self):
|
||||
try:
|
||||
self._execute('which', 'rbd')
|
||||
except putils.ProcessExecutionError:
|
||||
msg = 'ceph-common package not installed'
|
||||
raise exception.BrickException(msg)
|
||||
|
||||
RBDConnector.im_root = os.getuid() == 0
|
||||
# Check if we are running containerized
|
||||
RBDConnector.containerized = self._in_container()
|
||||
|
||||
# Don't check again to speed things on following connections
|
||||
RBDConnector._setup_rbd_class = lambda *args: None
|
||||
|
||||
def extend_volume(self, connection_properties):
|
||||
"""Refresh local volume view and return current size in bytes."""
|
||||
# Nothing to do, RBD attached volumes are automatically refreshed, but
|
||||
# we need to return the new size for compatibility
|
||||
link_name, real_dev_path = self._get_vol_data(connection_properties)
|
||||
|
||||
device_name = os.path.basename(real_dev_path) # ie: rbd0
|
||||
device_number = device_name[3:] # ie: 0
|
||||
# Get size from /sys/devices/rbd/0/size instead of
|
||||
# /sys/class/block/rbd0/size because the latter isn't updated
|
||||
with open('/sys/devices/rbd/' + device_number + '/size') as f:
|
||||
size_bytes = f.read().strip()
|
||||
return int(size_bytes)
|
||||
|
||||
_setup_rbd_class = _setup_class
|
||||
|
||||
|
||||
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
|
||||
|
||||
error_msg = 'Some unlinks failed for %s'
|
||||
if os.getuid() == 0:
|
||||
for link in links:
|
||||
with exc.context(catch_exception, error_msg, links):
|
||||
try:
|
||||
os.unlink(link)
|
||||
except OSError as exc:
|
||||
# Ignore file doesn't exist errors
|
||||
if exc.errno != errno.ENOENT:
|
||||
raise
|
||||
else:
|
||||
with exc.context(catch_exception, error_msg, links):
|
||||
# Ignore file doesn't exist errors
|
||||
putils.execute('rm', '-f', *links, run_as_root=True,
|
||||
root_helper=ROOT_HELPER)
|
||||
|
||||
if not no_errors and raise_at_end and exc:
|
||||
raise exc
|
||||
|
||||
|
||||
def _execute(*cmd, **kwargs):
|
||||
try:
|
||||
return rootwrap.custom_execute(*cmd, **kwargs)
|
||||
except OSError as e:
|
||||
sanitized_cmd = strutils.mask_password(' '.join(cmd))
|
||||
raise putils.ProcessExecutionError(
|
||||
cmd=sanitized_cmd, description=six.text_type(e))
|
||||
|
||||
|
||||
def init(root_helper='sudo'):
|
||||
global ROOT_HELPER
|
||||
ROOT_HELPER = root_helper
|
||||
priv_context.init(root_helper=[root_helper])
|
||||
|
||||
brick_get_connector_properties = connector.get_connector_properties
|
||||
brick_connector_factory = connector.InitiatorConnector.factory
|
||||
|
||||
def my_get_connector_properties(*args, **kwargs):
|
||||
if len(args):
|
||||
args = list(args)
|
||||
args[0] = ROOT_HELPER
|
||||
else:
|
||||
kwargs['root_helper'] = ROOT_HELPER
|
||||
kwargs['execute'] = _execute
|
||||
return brick_get_connector_properties(*args, **kwargs)
|
||||
|
||||
def my_connector_factory(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'] = _execute
|
||||
|
||||
# OS-Brick's implementation for RBD is not good enough for us
|
||||
if protocol == 'rbd':
|
||||
factory = RBDConnector
|
||||
else:
|
||||
factory = functools.partial(brick_connector_factory, protocol)
|
||||
|
||||
return factory(*args, **kwargs)
|
||||
|
||||
# Replace OS-Brick method and the reference we have to it
|
||||
connector.get_connector_properties = my_get_connector_properties
|
||||
cinderlib.get_connector_properties = my_get_connector_properties
|
||||
connector.InitiatorConnector.factory = staticmethod(my_connector_factory)
|
||||
if hasattr(rootwrap, 'unlink_root'):
|
||||
rootwrap.unlink_root = unlink_root
|
|
@ -20,9 +20,9 @@ from cinder import context
|
|||
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.volume import volume_utils as volume_utils
|
||||
from os_brick import exception as brick_exception
|
||||
from os_brick import initiator as brick_initiator
|
||||
from os_brick.initiator import connector as brick_connector
|
||||
from oslo_config import cfg
|
||||
from oslo_log import log as logging
|
||||
from oslo_utils import timeutils
|
||||
|
@ -542,9 +542,7 @@ class Volume(NamedObject):
|
|||
return snap
|
||||
|
||||
def attach(self):
|
||||
connector_dict = brick_connector.get_connector_properties(
|
||||
self.backend_class.root_helper,
|
||||
cfg.CONF.my_ip,
|
||||
connector_dict = volume_utils.brick_get_connector_properties(
|
||||
self.backend.configuration.use_multipath_for_image_xfer,
|
||||
self.backend.configuration.enforce_multipath_for_image_xfer)
|
||||
conn = self.connect(connector_dict)
|
||||
|
@ -772,8 +770,8 @@ class Connection(Object, LazyVolumeAttr):
|
|||
if not self._connector:
|
||||
if not self.conn_info:
|
||||
return None
|
||||
self._connector = brick_connector.InitiatorConnector.factory(
|
||||
self.protocol, self.backend_class.root_helper,
|
||||
self._connector = volume_utils.brick_get_connector(
|
||||
self.protocol,
|
||||
use_multipath=self.use_multipath,
|
||||
device_scan_attempts=self.scan_attempts,
|
||||
# NOTE(geguileo): afaik only remotefs uses the connection info
|
||||
|
|
|
@ -258,13 +258,12 @@ class TestConnection(base.BaseTest):
|
|||
|
||||
@mock.patch('cinderlib.objects.Connection.conn_info')
|
||||
@mock.patch('cinderlib.objects.Connection.protocol')
|
||||
@mock.patch('os_brick.initiator.connector.InitiatorConnector.factory')
|
||||
@mock.patch('cinder.volume.volume_utils.brick_get_connector')
|
||||
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,
|
||||
|
|
|
@ -311,15 +311,13 @@ class TestVolume(base.BaseTest):
|
|||
self.assertEqual('error', snap.status)
|
||||
mock_create.assert_called_once_with(snap._ovo)
|
||||
|
||||
@mock.patch('os_brick.initiator.connector.get_connector_properties')
|
||||
@mock.patch('cinder.volume.volume_utils.brick_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)
|
||||
|
||||
|
@ -327,7 +325,7 @@ class TestVolume(base.BaseTest):
|
|||
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('cinder.volume.volume_utils.brick_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)
|
||||
|
@ -336,8 +334,6 @@ class TestVolume(base.BaseTest):
|
|||
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)
|
||||
|
||||
|
@ -345,7 +341,7 @@ class TestVolume(base.BaseTest):
|
|||
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('cinder.volume.volume_utils.brick_get_connector_properties')
|
||||
@mock.patch('cinderlib.objects.Volume.connect')
|
||||
def test_attach_error_attach(self, mock_connect, mock_conn_props,
|
||||
mock_disconnect):
|
||||
|
@ -356,8 +352,6 @@ class TestVolume(base.BaseTest):
|
|||
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)
|
||||
|
||||
|
|
|
@ -14,9 +14,11 @@
|
|||
# under the License.
|
||||
|
||||
import collections
|
||||
import configparser
|
||||
import os
|
||||
from unittest import mock
|
||||
|
||||
from cinder import utils
|
||||
import ddt
|
||||
from oslo_config import cfg
|
||||
|
||||
|
@ -168,16 +170,16 @@ class TestCinderlib(base.BaseTest):
|
|||
|
||||
self.assertIsNone(cfg._CachedArgumentParser().parse_args())
|
||||
|
||||
@mock.patch('cinderlib.Backend._set_priv_helper')
|
||||
@mock.patch('cinderlib.Backend._set_cinder_config')
|
||||
@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,
|
||||
mock_set_config):
|
||||
mock_coord, mock_disable_warn, mock_set_config,
|
||||
mock_priv_helper):
|
||||
cls = objects.Backend
|
||||
cls.global_initialization = False
|
||||
cinder_cfg = {'k': 'v', 'k2': 'v2'}
|
||||
|
@ -201,7 +203,6 @@ class TestCinderlib(base.BaseTest):
|
|||
|
||||
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)
|
||||
|
@ -209,9 +210,10 @@ class TestCinderlib(base.BaseTest):
|
|||
|
||||
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()
|
||||
|
||||
mock_priv_helper.assert_called_once_with(mock.sentinel.root_helper)
|
||||
|
||||
self.assertEqual(2, mock_disable_warn.call_count)
|
||||
self.assertTrue(cls.global_initialization)
|
||||
self.assertEqual(mock.sentinel.backend_info,
|
||||
|
@ -494,3 +496,158 @@ class TestCinderlib(base.BaseTest):
|
|||
self.backend._apply_backend_workarounds(cfg)
|
||||
self.assertEqual(mock_conf.list_all_sections.return_value,
|
||||
mock_conf.list_all_sections())
|
||||
|
||||
@mock.patch.dict(os.environ, {}, clear=True)
|
||||
@mock.patch('os.path.exists')
|
||||
@mock.patch('configparser.ConfigParser')
|
||||
@mock.patch('oslo_privsep.priv_context.init')
|
||||
def test__set_priv_helper_no_venv_sudo(self, mock_ctxt_init, mock_parser,
|
||||
mock_exists):
|
||||
original_helper_func = utils.get_root_helper
|
||||
|
||||
original_rootwrap_config = cfg.CONF.rootwrap_config
|
||||
rootwrap_config = '/etc/cinder/rootwrap.conf'
|
||||
# Not using set_override because it's not working as it should
|
||||
cfg.CONF.rootwrap_config = rootwrap_config
|
||||
|
||||
try:
|
||||
self.backend._set_priv_helper('sudo')
|
||||
|
||||
mock_exists.assert_not_called()
|
||||
mock_parser.assert_not_called()
|
||||
mock_ctxt_init.assert_not_called()
|
||||
self.assertIs(original_helper_func, utils.get_root_helper)
|
||||
self.assertIs(rootwrap_config, cfg.CONF.rootwrap_config)
|
||||
finally:
|
||||
cfg.CONF.rootwrap_config = original_rootwrap_config
|
||||
|
||||
@mock.patch('configparser.ConfigParser.read', mock.Mock())
|
||||
@mock.patch('configparser.ConfigParser.write', mock.Mock())
|
||||
@mock.patch('cinderlib.cinderlib.utils.__file__',
|
||||
'/.venv/lib/python3.7/site-packages/cinder')
|
||||
@mock.patch('cinderlib.cinderlib.os.environ', {'VIRTUAL_ENV': '/.venv'})
|
||||
@mock.patch('cinderlib.cinderlib.open')
|
||||
@mock.patch('os.path.exists', return_value=False)
|
||||
@mock.patch('oslo_privsep.priv_context.init')
|
||||
def test__set_priv_helper_venv_no_sudo(self, mock_ctxt_init, mock_exists,
|
||||
mock_open):
|
||||
|
||||
file_contents = {'DEFAULT': {'filters_path': '/etc/cinder/rootwrap.d',
|
||||
'exec_dirs': '/dir1,/dir2'}}
|
||||
parser = configparser.ConfigParser()
|
||||
|
||||
venv_wrap_cfg = '/.venv/etc/cinder/rootwrap.conf'
|
||||
|
||||
original_helper_func = utils.get_root_helper
|
||||
original_rootwrap_config = cfg.CONF.rootwrap_config
|
||||
# Not using set_override because it's not working as it should
|
||||
default_wrap_cfg = '/etc/cinder/rootwrap.conf'
|
||||
cfg.CONF.rootwrap_config = default_wrap_cfg
|
||||
|
||||
try:
|
||||
with mock.patch('cinder.utils.get_root_helper',
|
||||
return_value='sudo wrapper') as mock_helper, \
|
||||
mock.patch.dict(parser, file_contents, clear=True), \
|
||||
mock.patch('configparser.ConfigParser') as mock_parser:
|
||||
mock_parser.return_value = parser
|
||||
self.backend._set_priv_helper('mysudo')
|
||||
|
||||
mock_exists.assert_called_once_with(default_wrap_cfg)
|
||||
|
||||
mock_parser.assert_called_once_with()
|
||||
parser.read.assert_called_once_with(venv_wrap_cfg)
|
||||
|
||||
self.assertEqual('/.venv/etc/cinder/rootwrap.d',
|
||||
parser['DEFAULT']['filters_path'])
|
||||
self.assertEqual('/.venv/bin,/dir1,/dir2',
|
||||
parser['DEFAULT']['exec_dirs'])
|
||||
|
||||
mock_open.assert_called_once_with(venv_wrap_cfg, 'w')
|
||||
parser.write.assert_called_once_with(
|
||||
mock_open.return_value.__enter__.return_value)
|
||||
|
||||
self.assertEqual('mysudo wrapper', utils.get_root_helper())
|
||||
|
||||
mock_helper.assert_called_once_with()
|
||||
mock_ctxt_init.assert_called_once_with(root_helper=['mysudo'])
|
||||
|
||||
self.assertIs(original_helper_func, utils.get_root_helper)
|
||||
self.assertEqual(venv_wrap_cfg, cfg.CONF.rootwrap_config)
|
||||
finally:
|
||||
cfg.CONF.rootwrap_config = original_rootwrap_config
|
||||
utils.get_root_helper = original_helper_func
|
||||
|
||||
@mock.patch('configparser.ConfigParser.read', mock.Mock())
|
||||
@mock.patch('configparser.ConfigParser.write', mock.Mock())
|
||||
@mock.patch('cinderlib.cinderlib.utils.__file__', '/opt/stack/cinder')
|
||||
@mock.patch('cinderlib.cinderlib.os.environ', {'VIRTUAL_ENV': '/.venv'})
|
||||
@mock.patch('shutil.copytree')
|
||||
@mock.patch('glob.glob',)
|
||||
@mock.patch('cinderlib.cinderlib.open')
|
||||
@mock.patch('os.path.exists', return_value=False)
|
||||
@mock.patch('oslo_privsep.priv_context.init')
|
||||
def test__set_priv_helper_venv_editable_no_sudo(self, mock_ctxt_init,
|
||||
mock_exists, mock_open,
|
||||
mock_glob, mock_copy):
|
||||
|
||||
link_file = '/.venv/lib/python3.7/site-packages/cinder.egg-link'
|
||||
cinder_source_path = '/opt/stack/cinder'
|
||||
link_file_contents = cinder_source_path + '\n.'
|
||||
mock_glob.return_value = [link_file]
|
||||
open_fd = mock_open.return_value.__enter__.return_value
|
||||
open_fd.read.return_value = link_file_contents
|
||||
|
||||
file_contents = {'DEFAULT': {'filters_path': '/etc/cinder/rootwrap.d',
|
||||
'exec_dirs': '/dir1,/dir2'}}
|
||||
parser = configparser.ConfigParser()
|
||||
|
||||
venv_wrap_cfg = '/.venv/etc/cinder/rootwrap.conf'
|
||||
|
||||
original_helper_func = utils.get_root_helper
|
||||
original_rootwrap_config = cfg.CONF.rootwrap_config
|
||||
# Not using set_override because it's not working as it should
|
||||
default_wrap_cfg = '/etc/cinder/rootwrap.conf'
|
||||
cfg.CONF.rootwrap_config = default_wrap_cfg
|
||||
|
||||
try:
|
||||
with mock.patch('cinder.utils.get_root_helper',
|
||||
return_value='sudo wrapper') as mock_helper, \
|
||||
mock.patch.dict(parser, file_contents, clear=True), \
|
||||
mock.patch('configparser.ConfigParser') as mock_parser:
|
||||
mock_parser.return_value = parser
|
||||
|
||||
self.backend._set_priv_helper('mysudo')
|
||||
|
||||
mock_glob.assert_called_once_with(
|
||||
'/.venv/lib/python*/site-packages/cinder.egg-link')
|
||||
|
||||
self.assertEqual(2, mock_exists.call_count)
|
||||
mock_exists.assert_has_calls([mock.call(default_wrap_cfg),
|
||||
mock.call(venv_wrap_cfg)])
|
||||
|
||||
self.assertEqual(2, mock_open.call_count)
|
||||
mock_open.assert_any_call(link_file, 'r')
|
||||
mock_copy.assert_called_once_with(
|
||||
cinder_source_path + '/etc/cinder', '/.venv/etc/cinder')
|
||||
|
||||
mock_parser.assert_called_once_with()
|
||||
parser.read.assert_called_once_with(venv_wrap_cfg)
|
||||
|
||||
self.assertEqual('/.venv/etc/cinder/rootwrap.d',
|
||||
parser['DEFAULT']['filters_path'])
|
||||
self.assertEqual('/.venv/bin,/dir1,/dir2',
|
||||
parser['DEFAULT']['exec_dirs'])
|
||||
|
||||
mock_open.assert_any_call(venv_wrap_cfg, 'w')
|
||||
parser.write.assert_called_once_with(open_fd)
|
||||
|
||||
self.assertEqual('mysudo wrapper', utils.get_root_helper())
|
||||
|
||||
mock_helper.assert_called_once_with()
|
||||
mock_ctxt_init.assert_called_once_with(root_helper=['mysudo'])
|
||||
|
||||
self.assertIs(original_helper_func, utils.get_root_helper)
|
||||
self.assertEqual(venv_wrap_cfg, cfg.CONF.rootwrap_config)
|
||||
finally:
|
||||
cfg.CONF.rootwrap_config = original_rootwrap_config
|
||||
utils.get_root_helper = original_helper_func
|
||||
|
|
|
@ -1,497 +0,0 @@
|
|||
# Copyright (c) 2019, 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 errno
|
||||
from unittest import mock
|
||||
|
||||
import ddt
|
||||
from os_brick import exception
|
||||
from oslo_concurrency import processutils as putils
|
||||
|
||||
from cinderlib import nos_brick
|
||||
from cinderlib.tests.unit import base
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
class TestRBDConnector(base.BaseTest):
|
||||
def setUp(self):
|
||||
self.connector = nos_brick.RBDConnector('sudo')
|
||||
self.connector.im_root = False
|
||||
self.connector.containerized = False
|
||||
self.connector._setup_rbd_class = lambda *args: None
|
||||
|
||||
@mock.patch.object(nos_brick, 'open')
|
||||
@mock.patch('os.stat')
|
||||
def test__in_container_stat(self, mock_stat, mock_open):
|
||||
mock_stat.return_value.st_dev = 4
|
||||
res = self.connector._in_container()
|
||||
self.assertFalse(res)
|
||||
mock_stat.assert_called_once_with('/proc')
|
||||
mock_open.assert_not_called()
|
||||
|
||||
@mock.patch.object(nos_brick, 'open')
|
||||
@mock.patch('os.stat')
|
||||
def test__in_container_mounts_no_container(self, mock_stat, mock_open):
|
||||
mock_stat.return_value.st_dev = 5
|
||||
mock_read = mock_open.return_value.__enter__.return_value.readlines
|
||||
mock_read.return_value = [
|
||||
'sysfs /sys sysfs rw,seclabel,nosuid,nodev,noexec,relatime 0 0',
|
||||
'proc /proc proc rw,nosuid,nodev,noexec,relatime 0 0',
|
||||
'/dev/mapper/fedora_think2-root / ext4 rw,seclabel,relatime 0 0',
|
||||
'selinuxfs /sys/fs/selinux selinuxfs rw,relatime 0 0',
|
||||
]
|
||||
|
||||
res = self.connector._in_container()
|
||||
self.assertFalse(res)
|
||||
mock_stat.assert_called_once_with('/proc')
|
||||
mock_open.assert_called_once_with('/proc/1/mounts', 'r')
|
||||
mock_read.assert_called_once_with()
|
||||
|
||||
@mock.patch.object(nos_brick.LOG, 'warning')
|
||||
@mock.patch.object(nos_brick, 'open')
|
||||
@mock.patch('os.stat')
|
||||
def test__in_container_mounts_in_container(self, mock_stat, mock_open,
|
||||
mock_warning):
|
||||
mock_stat.return_value.st_dev = 5
|
||||
mock_read = mock_open.return_value.__enter__.return_value.readlines
|
||||
mock_read.return_value = [
|
||||
'sysfs /sys sysfs rw,seclabel,nosuid,nodev,noexec,relatime 0 0',
|
||||
'proc /proc proc rw,nosuid,nodev,noexec,relatime 0 0',
|
||||
'overlay / overlay rw,lowerdir=/var/lib/containers/...',
|
||||
'selinuxfs /sys/fs/selinux selinuxfs rw,relatime 0 0',
|
||||
]
|
||||
|
||||
res = self.connector._in_container()
|
||||
self.assertTrue(res)
|
||||
mock_stat.assert_called_once_with('/proc')
|
||||
mock_open.assert_called_once_with('/proc/1/mounts', 'r')
|
||||
mock_read.assert_called_once_with()
|
||||
mock_warning.assert_not_called()
|
||||
|
||||
@mock.patch.object(nos_brick.RBDConnector, '_get_rbd_args')
|
||||
@mock.patch.object(nos_brick.RBDConnector, '_execute')
|
||||
@mock.patch('os.path.exists', return_value=True)
|
||||
def test__unmap_exists(self, exists_mock, exec_mock, args_mock):
|
||||
args_mock.return_value = [mock.sentinel.args]
|
||||
self.connector._unmap(mock.sentinel.path, mock.sentinel.conf,
|
||||
mock.sentinel.conn_props)
|
||||
exists_mock.assert_called_once_with(mock.sentinel.path)
|
||||
exec_mock.assert_called_once_with(
|
||||
'rbd', 'unmap', mock.sentinel.path, '--conf', mock.sentinel.conf,
|
||||
mock.sentinel.args, root_helper='sudo', run_as_root=True)
|
||||
|
||||
@mock.patch.object(nos_brick.RBDConnector, '_execute')
|
||||
@mock.patch('os.path.exists', return_value=False)
|
||||
def test__unmap_doesnt_exist(self, exists_mock, exec_mock):
|
||||
self.connector._unmap(mock.sentinel.path, mock.sentinel.conf,
|
||||
mock.sentinel.conn_props)
|
||||
exists_mock.assert_called_once_with(mock.sentinel.path)
|
||||
exec_mock.assert_not_called()
|
||||
|
||||
@ddt.data(True, False)
|
||||
@mock.patch('oslo_utils.fileutils.delete_if_exists')
|
||||
@mock.patch('cinderlib.nos_brick.unlink_root')
|
||||
@mock.patch('os.path.realpath')
|
||||
@mock.patch.object(nos_brick.RBDConnector, 'get_rbd_device_name')
|
||||
@mock.patch.object(nos_brick.RBDConnector, '_unmap')
|
||||
def test_disconnect_volume(self, is_containerized, unmap_mock,
|
||||
dev_name_mock, path_mock, unlink_mock,
|
||||
delete_mock):
|
||||
self.connector.containerized = is_containerized
|
||||
conn_props = {'name': 'pool/volume'}
|
||||
dev_info = {'conf': mock.sentinel.conf_file}
|
||||
self.connector.disconnect_volume(conn_props, dev_info)
|
||||
dev_name_mock.assert_called_once_with('pool', 'volume')
|
||||
path_mock.assert_called_once_with(dev_name_mock.return_value)
|
||||
unmap_mock.assert_called_once_with(path_mock.return_value,
|
||||
mock.sentinel.conf_file,
|
||||
conn_props)
|
||||
if is_containerized:
|
||||
unlink_mock.assert_called_once_with(dev_name_mock.return_value)
|
||||
else:
|
||||
unlink_mock.assert_not_called()
|
||||
delete_mock.assert_called_once_with(mock.sentinel.conf_file)
|
||||
|
||||
@mock.patch.object(nos_brick.RBDConnector, '_execute')
|
||||
@mock.patch('os.path.islink')
|
||||
@mock.patch('os.path.exists')
|
||||
@mock.patch('os.path.realpath')
|
||||
@mock.patch.object(nos_brick.RBDConnector, 'get_rbd_device_name')
|
||||
@mock.patch.object(nos_brick.RBDConnector, '_create_ceph_conf')
|
||||
def test_connect_volume_exists(self, conf_mock, dev_name_mock, path_mock,
|
||||
exists_mock, islink_mock, exec_mock):
|
||||
conn_props = {'auth_username': mock.sentinel.username,
|
||||
'name': 'pool/volume',
|
||||
'cluster_name': mock.sentinel.cluster_name,
|
||||
'hosts': mock.sentinel.hosts,
|
||||
'ports': mock.sentinel.ports,
|
||||
'keyring': mock.sentinel.keyring}
|
||||
result = self.connector.connect_volume(conn_props)
|
||||
conf_mock.assert_called_once_with(mock.sentinel.hosts,
|
||||
mock.sentinel.ports,
|
||||
'sentinel.cluster_name',
|
||||
mock.sentinel.username,
|
||||
mock.sentinel.keyring)
|
||||
dev_name_mock.assert_called_once_with('pool', 'volume')
|
||||
path_mock.assert_called_once_with(dev_name_mock.return_value)
|
||||
islink_mock.assert_called_with(dev_name_mock.return_value)
|
||||
exists_mock.assert_called_once_with(path_mock.return_value)
|
||||
exec_mock.assert_not_called()
|
||||
|
||||
expected = {'path': path_mock.return_value,
|
||||
'conf': conf_mock.return_value,
|
||||
'type': 'block'}
|
||||
self.assertEqual(expected, result)
|
||||
|
||||
@ddt.data(True, False)
|
||||
@mock.patch.object(nos_brick.RBDConnector, '_ensure_link')
|
||||
@mock.patch.object(nos_brick.RBDConnector, '_get_rbd_args')
|
||||
@mock.patch.object(nos_brick.RBDConnector, '_execute')
|
||||
@mock.patch('os.path.islink')
|
||||
@mock.patch('os.path.exists', return_value=False)
|
||||
@mock.patch('os.path.realpath')
|
||||
@mock.patch.object(nos_brick.RBDConnector, 'get_rbd_device_name')
|
||||
@mock.patch.object(nos_brick.RBDConnector, '_create_ceph_conf')
|
||||
def test_connect_volume(self, is_containerized, conf_mock, dev_name_mock,
|
||||
path_mock, exists_mock, islink_mock, exec_mock,
|
||||
args_mock, link_mock):
|
||||
exec_mock.return_value = (' a ', '')
|
||||
args_mock.return_value = [mock.sentinel.args]
|
||||
self.connector.containerized = is_containerized
|
||||
conn_props = {'auth_username': mock.sentinel.username,
|
||||
'name': 'pool/volume',
|
||||
'cluster_name': mock.sentinel.cluster_name,
|
||||
'hosts': mock.sentinel.hosts,
|
||||
'ports': mock.sentinel.ports,
|
||||
'keyring': mock.sentinel.keyring}
|
||||
result = self.connector.connect_volume(conn_props)
|
||||
conf_mock.assert_called_once_with(mock.sentinel.hosts,
|
||||
mock.sentinel.ports,
|
||||
'sentinel.cluster_name',
|
||||
mock.sentinel.username,
|
||||
mock.sentinel.keyring)
|
||||
dev_name_mock.assert_called_once_with('pool', 'volume')
|
||||
path_mock.assert_called_once_with(dev_name_mock.return_value)
|
||||
islink_mock.assert_called_with(dev_name_mock.return_value)
|
||||
exists_mock.assert_called_once_with(path_mock.return_value)
|
||||
exec_mock.assert_called_once_with(
|
||||
'rbd', 'map', 'volume', '--pool', 'pool', '--conf',
|
||||
conf_mock.return_value, mock.sentinel.args,
|
||||
root_helper='sudo', run_as_root=True)
|
||||
if is_containerized:
|
||||
link_mock.assert_called_once_with('a', dev_name_mock.return_value)
|
||||
else:
|
||||
link_mock.assert_not_called()
|
||||
expected = {'path': 'a',
|
||||
'conf': conf_mock.return_value,
|
||||
'type': 'block'}
|
||||
self.assertEqual(expected, result)
|
||||
|
||||
@mock.patch('oslo_utils.fileutils.delete_if_exists')
|
||||
@mock.patch.object(nos_brick.RBDConnector, '_unmap')
|
||||
@mock.patch.object(nos_brick.RBDConnector, '_ensure_link')
|
||||
@mock.patch.object(nos_brick.RBDConnector, '_get_rbd_args')
|
||||
@mock.patch.object(nos_brick.RBDConnector, '_execute')
|
||||
@mock.patch('os.path.islink')
|
||||
@mock.patch('os.path.exists', return_value=False)
|
||||
@mock.patch('os.path.realpath')
|
||||
@mock.patch.object(nos_brick.RBDConnector, 'get_rbd_device_name')
|
||||
@mock.patch.object(nos_brick.RBDConnector, '_create_ceph_conf')
|
||||
def test_connect_volume_map_fail(self, conf_mock, dev_name_mock, path_mock,
|
||||
exists_mock, islink_mock, exec_mock,
|
||||
args_mock, link_mock, unmap_mock,
|
||||
delete_mock):
|
||||
exec_mock.side_effect = Exception
|
||||
unmap_mock.side_effect = Exception
|
||||
args_mock.return_value = [mock.sentinel.args]
|
||||
conn_props = {'auth_username': mock.sentinel.username,
|
||||
'name': 'pool/volume',
|
||||
'cluster_name': mock.sentinel.cluster_name,
|
||||
'hosts': mock.sentinel.hosts,
|
||||
'ports': mock.sentinel.ports,
|
||||
'keyring': mock.sentinel.keyring}
|
||||
with self.assertRaises(exception.BrickException):
|
||||
self.connector.connect_volume(conn_props)
|
||||
link_mock.assert_not_called()
|
||||
conf_mock.assert_called_once_with(mock.sentinel.hosts,
|
||||
mock.sentinel.ports,
|
||||
'sentinel.cluster_name',
|
||||
mock.sentinel.username,
|
||||
mock.sentinel.keyring)
|
||||
dev_name_mock.assert_called_once_with('pool', 'volume')
|
||||
path_mock.assert_called_once_with(dev_name_mock.return_value)
|
||||
islink_mock.assert_called_with(dev_name_mock.return_value)
|
||||
exists_mock.assert_called_once_with(path_mock.return_value)
|
||||
exec_mock.assert_called_once_with(
|
||||
'rbd', 'map', 'volume', '--pool', 'pool', '--conf',
|
||||
conf_mock.return_value, mock.sentinel.args, root_helper='sudo',
|
||||
run_as_root=True)
|
||||
unmap_mock.assert_called_once_with(path_mock.return_value,
|
||||
conf_mock.return_value,
|
||||
conn_props)
|
||||
delete_mock.assert_called_once_with(conf_mock.return_value)
|
||||
|
||||
@mock.patch('oslo_utils.fileutils.delete_if_exists')
|
||||
@mock.patch.object(nos_brick.RBDConnector, '_unmap')
|
||||
@mock.patch.object(nos_brick.RBDConnector, '_ensure_link')
|
||||
@mock.patch.object(nos_brick.RBDConnector, '_get_rbd_args')
|
||||
@mock.patch.object(nos_brick.RBDConnector, '_execute')
|
||||
@mock.patch('os.path.islink')
|
||||
@mock.patch('os.path.exists', return_value=False)
|
||||
@mock.patch('os.path.realpath')
|
||||
@mock.patch.object(nos_brick.RBDConnector, 'get_rbd_device_name')
|
||||
@mock.patch.object(nos_brick.RBDConnector, '_create_ceph_conf')
|
||||
def test_connect_volume_link_fail(self, conf_mock, dev_name_mock,
|
||||
path_mock, exists_mock, islink_mock,
|
||||
exec_mock, args_mock, link_mock,
|
||||
unmap_mock, delete_mock):
|
||||
exec_mock.return_value = (' a ', '')
|
||||
link_mock.side_effect = Exception
|
||||
self.connector.containerized = True
|
||||
args_mock.return_value = [mock.sentinel.args]
|
||||
conn_props = {'auth_username': mock.sentinel.username,
|
||||
'name': 'pool/volume',
|
||||
'cluster_name': mock.sentinel.cluster_name,
|
||||
'hosts': mock.sentinel.hosts,
|
||||
'ports': mock.sentinel.ports,
|
||||
'keyring': mock.sentinel.keyring}
|
||||
with self.assertRaises(exception.BrickException):
|
||||
self.connector.connect_volume(conn_props)
|
||||
link_mock.assert_called_once_with('a', dev_name_mock.return_value)
|
||||
conf_mock.assert_called_once_with(mock.sentinel.hosts,
|
||||
mock.sentinel.ports,
|
||||
'sentinel.cluster_name',
|
||||
mock.sentinel.username,
|
||||
mock.sentinel.keyring)
|
||||
dev_name_mock.assert_called_once_with('pool', 'volume')
|
||||
path_mock.assert_called_once_with(dev_name_mock.return_value)
|
||||
islink_mock.assert_called_with(dev_name_mock.return_value)
|
||||
exists_mock.assert_called_once_with(path_mock.return_value)
|
||||
exec_mock.assert_called_once_with(
|
||||
'rbd', 'map', 'volume', '--pool', 'pool', '--conf',
|
||||
conf_mock.return_value, mock.sentinel.args, root_helper='sudo',
|
||||
run_as_root=True)
|
||||
unmap_mock.assert_called_once_with('a',
|
||||
conf_mock.return_value,
|
||||
conn_props)
|
||||
delete_mock.assert_called_once_with(conf_mock.return_value)
|
||||
|
||||
@mock.patch.object(nos_brick.RBDConnector, '_execute')
|
||||
@mock.patch('os.makedirs')
|
||||
def test__ensure_dir(self, mkdir_mock, exec_mock):
|
||||
self.connector._ensure_dir(mock.sentinel.path)
|
||||
exec_mock.assert_called_once_with(
|
||||
'mkdir', '-p', '-m0755', mock.sentinel.path,
|
||||
root_helper=self.connector._root_helper, run_as_root=True)
|
||||
mkdir_mock.assert_not_called()
|
||||
|
||||
@mock.patch.object(nos_brick.RBDConnector, '_execute')
|
||||
@mock.patch('os.makedirs')
|
||||
def test__ensure_dir_root(self, mkdir_mock, exec_mock):
|
||||
self.connector.im_root = True
|
||||
self.connector._ensure_dir(mock.sentinel.path)
|
||||
mkdir_mock.assert_called_once_with(mock.sentinel.path, 0o755)
|
||||
exec_mock.assert_not_called()
|
||||
|
||||
@mock.patch.object(nos_brick.RBDConnector, '_execute')
|
||||
@mock.patch('os.makedirs', side_effect=OSError(errno.EEXIST, ''))
|
||||
def test__ensure_dir_root_exists(self, mkdir_mock, exec_mock):
|
||||
self.connector.im_root = True
|
||||
self.connector._ensure_dir(mock.sentinel.path)
|
||||
mkdir_mock.assert_called_once_with(mock.sentinel.path, 0o755)
|
||||
exec_mock.assert_not_called()
|
||||
|
||||
@mock.patch.object(nos_brick.RBDConnector, '_execute')
|
||||
@mock.patch('os.makedirs', side_effect=OSError(errno.EPERM, ''))
|
||||
def test__ensure_dir_root_fails(self, mkdir_mock, exec_mock):
|
||||
self.connector.im_root = True
|
||||
with self.assertRaises(OSError) as exc:
|
||||
self.connector._ensure_dir(mock.sentinel.path)
|
||||
self.assertEqual(mkdir_mock.side_effect, exc.exception)
|
||||
mkdir_mock.assert_called_once_with(mock.sentinel.path, 0o755)
|
||||
exec_mock.assert_not_called()
|
||||
|
||||
@mock.patch('os.path.exists')
|
||||
@mock.patch('os.remove')
|
||||
@mock.patch.object(nos_brick.RBDConnector, '_execute')
|
||||
@mock.patch.object(nos_brick.RBDConnector, '_ensure_dir')
|
||||
@mock.patch('os.symlink')
|
||||
def test__ensure_link(self, link_mock, dir_mock, exec_mock, remove_mock,
|
||||
exists_mock):
|
||||
source = '/dev/rbd0'
|
||||
link = '/dev/rbd/rbd/volume-xyz'
|
||||
self.connector._ensure_link(source, link)
|
||||
dir_mock.assert_called_once_with('/dev/rbd/rbd')
|
||||
exec_mock.assert_called_once_with(
|
||||
'ln', '-s', '-f', source, link,
|
||||
root_helper=self.connector._root_helper, run_as_root=True)
|
||||
exists_mock.assert_not_called()
|
||||
remove_mock.assert_not_called()
|
||||
link_mock.assert_not_called()
|
||||
|
||||
@mock.patch('os.path.exists', return_value=False)
|
||||
@mock.patch('os.remove')
|
||||
@mock.patch.object(nos_brick.RBDConnector, '_execute')
|
||||
@mock.patch.object(nos_brick.RBDConnector, '_ensure_dir')
|
||||
@mock.patch('os.symlink')
|
||||
def test__ensure_link_root(self, link_mock, dir_mock, exec_mock,
|
||||
remove_mock, exists_mock):
|
||||
self.connector.im_root = True
|
||||
source = '/dev/rbd0'
|
||||
link = '/dev/rbd/rbd/volume-xyz'
|
||||
self.connector._ensure_link(source, link)
|
||||
dir_mock.assert_called_once_with('/dev/rbd/rbd')
|
||||
exec_mock.assert_not_called()
|
||||
remove_mock.assert_not_called()
|
||||
exists_mock.assert_called_once_with(link)
|
||||
link_mock.assert_called_once_with(source, link)
|
||||
|
||||
@mock.patch('os.path.exists', return_value=False)
|
||||
@mock.patch('os.remove')
|
||||
@mock.patch.object(nos_brick.RBDConnector, '_execute')
|
||||
@mock.patch.object(nos_brick.RBDConnector, '_ensure_dir')
|
||||
@mock.patch('os.symlink', side_effect=OSError(errno.EEXIST, ''))
|
||||
def test__ensure_link_root_appears(self, link_mock, dir_mock, exec_mock,
|
||||
remove_mock, exists_mock):
|
||||
self.connector.im_root = True
|
||||
source = '/dev/rbd0'
|
||||
link = '/dev/rbd/rbd/volume-xyz'
|
||||
self.connector._ensure_link(source, link)
|
||||
dir_mock.assert_called_once_with('/dev/rbd/rbd')
|
||||
exec_mock.assert_not_called()
|
||||
exists_mock.assert_called_once_with(link)
|
||||
remove_mock.assert_not_called()
|
||||
link_mock.assert_called_once_with(source, link)
|
||||
|
||||
@mock.patch('os.path.exists', return_value=False)
|
||||
@mock.patch('os.remove')
|
||||
@mock.patch.object(nos_brick.RBDConnector, '_execute')
|
||||
@mock.patch.object(nos_brick.RBDConnector, '_ensure_dir')
|
||||
@mock.patch('os.symlink', side_effect=OSError(errno.EPERM, ''))
|
||||
def test__ensure_link_root_fails(self, link_mock, dir_mock, exec_mock,
|
||||
remove_mock, exists_mock):
|
||||
self.connector.im_root = True
|
||||
source = '/dev/rbd0'
|
||||
link = '/dev/rbd/rbd/volume-xyz'
|
||||
|
||||
with self.assertRaises(OSError) as exc:
|
||||
self.connector._ensure_link(source, link)
|
||||
|
||||
self.assertEqual(link_mock.side_effect, exc.exception)
|
||||
dir_mock.assert_called_once_with('/dev/rbd/rbd')
|
||||
exec_mock.assert_not_called()
|
||||
exists_mock.assert_called_once_with(link)
|
||||
remove_mock.assert_not_called()
|
||||
link_mock.assert_called_once_with(source, link)
|
||||
|
||||
@mock.patch('os.path.exists')
|
||||
@mock.patch('os.remove')
|
||||
@mock.patch('os.path.realpath')
|
||||
@mock.patch.object(nos_brick.RBDConnector, '_execute')
|
||||
@mock.patch.object(nos_brick.RBDConnector, '_ensure_dir')
|
||||
@mock.patch('os.symlink', side_effect=[OSError(errno.EEXIST, ''), None])
|
||||
def test__ensure_link_root_replace(self, link_mock, dir_mock, exec_mock,
|
||||
path_mock, remove_mock, exists_mock):
|
||||
self.connector.im_root = True
|
||||
source = '/dev/rbd0'
|
||||
path_mock.return_value = '/dev/rbd1'
|
||||
link = '/dev/rbd/rbd/volume-xyz'
|
||||
self.connector._ensure_link(source, link)
|
||||
dir_mock.assert_called_once_with('/dev/rbd/rbd')
|
||||
exec_mock.assert_not_called()
|
||||
exists_mock.assert_called_once_with(link)
|
||||
remove_mock.assert_called_once_with(link)
|
||||
link_mock.assert_called_once_with(source, link)
|
||||
|
||||
@mock.patch('six.moves.builtins.open')
|
||||
@mock.patch.object(nos_brick.RBDConnector, '_get_vol_data')
|
||||
def test_extend_volume(self, get_data_mock, open_mock):
|
||||
get_data_mock.return_value = (
|
||||
'/dev/rbd/rbd/volume-56539d26-2b78-49b8-8b96-160a62b0831f',
|
||||
'/dev/rbd10')
|
||||
|
||||
cm_open = open_mock.return_value.__enter__.return_value
|
||||
cm_open.read.return_value = '5368709120'
|
||||
res = self.connector.extend_volume(mock.sentinel.connector_properties)
|
||||
|
||||
self.assertEqual(5 * (1024 ** 3), res) # 5 GBi
|
||||
get_data_mock.assert_called_once_with(
|
||||
mock.sentinel.connector_properties)
|
||||
open_mock.assert_called_once_with('/sys/devices/rbd/10/size')
|
||||
|
||||
@mock.patch('six.moves.builtins.open')
|
||||
def test_check_valid_device_root(self, open_mock):
|
||||
self.connector.im_root = True
|
||||
res = self.connector.check_valid_device(mock.sentinel.path)
|
||||
self.assertTrue(res)
|
||||
open_mock.assert_called_once_with(mock.sentinel.path, 'rb')
|
||||
read_mock = open_mock.return_value.__enter__.return_value.read
|
||||
read_mock.assert_called_once_with(4096)
|
||||
|
||||
@mock.patch('six.moves.builtins.open')
|
||||
def test_check_valid_device_root_fail_open(self, open_mock):
|
||||
self.connector.im_root = True
|
||||
open_mock.side_effect = OSError
|
||||
res = self.connector.check_valid_device(mock.sentinel.path)
|
||||
self.assertFalse(res)
|
||||
open_mock.assert_called_once_with(mock.sentinel.path, 'rb')
|
||||
read_mock = open_mock.return_value.__enter__.return_value.read
|
||||
read_mock.assert_not_called()
|
||||
|
||||
@mock.patch('six.moves.builtins.open')
|
||||
def test_check_valid_device_root_fail_read(self, open_mock):
|
||||
self.connector.im_root = True
|
||||
read_mock = open_mock.return_value.__enter__.return_value.read
|
||||
read_mock.side_effect = IOError
|
||||
res = self.connector.check_valid_device(mock.sentinel.path)
|
||||
self.assertFalse(res)
|
||||
open_mock.assert_called_once_with(mock.sentinel.path, 'rb')
|
||||
read_mock.assert_called_once_with(4096)
|
||||
|
||||
@mock.patch.object(nos_brick.RBDConnector, '_execute')
|
||||
def test_check_valid_device_non_root(self, exec_mock):
|
||||
res = self.connector.check_valid_device('/tmp/path')
|
||||
self.assertTrue(res)
|
||||
exec_mock.assert_called_once_with(
|
||||
'dd', 'if=/tmp/path', 'of=/dev/null', 'bs=4096', 'count=1',
|
||||
root_helper=self.connector._root_helper, run_as_root=True)
|
||||
|
||||
@mock.patch.object(nos_brick.RBDConnector, '_execute')
|
||||
def test_check_valid_device_non_root_fail(self, exec_mock):
|
||||
exec_mock.side_effect = putils.ProcessExecutionError
|
||||
res = self.connector.check_valid_device('/tmp/path')
|
||||
self.assertFalse(res)
|
||||
exec_mock.assert_called_once_with(
|
||||
'dd', 'if=/tmp/path', 'of=/dev/null', 'bs=4096', 'count=1',
|
||||
root_helper=self.connector._root_helper, run_as_root=True)
|
||||
|
||||
@mock.patch.object(nos_brick.os, 'unlink')
|
||||
@mock.patch.object(nos_brick.os, 'getuid', return_value=0)
|
||||
def test_unlink_root_being_root(self, mock_getuid, mock_unlink):
|
||||
mock_unlink.side_effect = [None, OSError(errno.ENOENT, '')]
|
||||
nos_brick.unlink_root(mock.sentinel.file1, mock.sentinel.file2)
|
||||
mock_getuid.assert_called_once()
|
||||
mock_unlink.assert_has_calls([mock.call(mock.sentinel.file1),
|
||||
mock.call(mock.sentinel.file2)])
|
||||
|
||||
@mock.patch.object(nos_brick.putils, 'execute')
|
||||
@mock.patch.object(nos_brick.os, 'getuid', return_value=1000)
|
||||
def test_unlink_root_non_root(self, mock_getuid, mock_exec):
|
||||
nos_brick.unlink_root(mock.sentinel.file1, mock.sentinel.file2)
|
||||
mock_getuid.assert_called_once()
|
||||
mock_exec.assert_called_once_with('rm', '-f', mock.sentinel.file1,
|
||||
mock.sentinel.file2,
|
||||
run_as_root=True,
|
||||
root_helper=nos_brick.ROOT_HELPER)
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
fixes:
|
||||
- |
|
||||
`Bug #1883720 <https://bugs.launchpad.net/cinderlib/+bug/1883720>`_: Added
|
||||
privsep support, increasing cinderlib's compatibility with Cinder drivers.
|
|
@ -5,4 +5,4 @@
|
|||
params=()
|
||||
for arg in "$@"; do params+=("\"$arg\""); done
|
||||
params="${params[@]}"
|
||||
sudo -E --preserve-env=PATH /bin/bash -c "$params"
|
||||
sudo -E --preserve-env=PATH,VIRTUAL_ENV /bin/bash -c "$params"
|
||||
|
|
Loading…
Reference in New Issue