Merge "Add privsep support"

This commit is contained in:
Zuul 2021-06-22 00:11:18 +00:00 committed by Gerrit Code Review
commit 4f4c8c15f5
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"