Add privsep support
Cinderlib does not support Cinder drivers that make use of the privsep library. Originally privsep had a limitation that would serialize all requests, so slow operations would create a bottleneck, thus cinderlib decided not to use privsep and call the commands directly. Since privsep no longer serializes requests we stop going around privsep and use it. Cinderlib is a library that works when run in a virtual environment, so we must maintain backward compatibiliy and still support it, which is problematic, because with Cinder's rootwrap+privsep we require /etc/cinder/rootwrap.conf and /etc/cinder/rootwrap.d to exist, but under a virtual env these are installed in the virtualenv's etc directory instead. For example: .tox/py37/etc/cinder/rootwrap.conf This configuration file is modified to point to the right filters directory and add the virtual env's bin directory to exec_dirs. We also take into account if we have installed cinder as editable in our virtual environment, because in that case files are not installed, and we will copy them from the source's directory into the virtual environment so we can freely modify them. Depends-On: https://review.opendev.org/737312 Closes-Bug: #1883720 Change-Id: I7963fbfbb0a683e3efcc5949f80b96e5daaa18f1
This commit is contained in:
parent
6ad3c4a19c
commit
cf638c41f6
|
@ -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