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:
Gorka Eguileor 2020-06-16 16:37:15 +02:00
parent 6ad3c4a19c
commit cf638c41f6
11 changed files with 254 additions and 837 deletions

View File

@ -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

View File

@ -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

View File

@ -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):

View File

@ -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

View File

@ -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

View File

@ -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,

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -0,0 +1,5 @@
---
fixes:
- |
`Bug #1883720 <https://bugs.launchpad.net/cinderlib/+bug/1883720>`_: Added
privsep support, increasing cinderlib's compatibility with Cinder drivers.

View File

@ -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"