From cf638c41f6aa8437aab14babc6406df2ceb0facc Mon Sep 17 00:00:00 2001 From: Gorka Eguileor Date: Tue, 16 Jun 2020 16:37:15 +0200 Subject: [PATCH] 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 --- .zuul.yaml | 1 - cinderlib/__init__.py | 4 +- cinderlib/cinderlib.py | 79 ++- cinderlib/nos_brick.py | 311 ----------- cinderlib/objects.py | 10 +- .../tests/unit/objects/test_connection.py | 3 +- cinderlib/tests/unit/objects/test_volume.py | 12 +- cinderlib/tests/unit/test_cinderlib.py | 167 +++++- cinderlib/tests/unit/test_nos_brick.py | 497 ------------------ .../privsep-support-acbc955b9845a6ef.yaml | 5 + tools/virtualenv-sudo.sh | 2 +- 11 files changed, 254 insertions(+), 837 deletions(-) delete mode 100644 cinderlib/nos_brick.py delete mode 100644 cinderlib/tests/unit/test_nos_brick.py create mode 100644 releasenotes/notes/privsep-support-acbc955b9845a6ef.yaml diff --git a/.zuul.yaml b/.zuul.yaml index e571868..cd864b8 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -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 diff --git a/cinderlib/__init__.py b/cinderlib/__init__.py index 37a1189..473af26 100644 --- a/cinderlib/__init__.py +++ b/cinderlib/__init__.py @@ -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 diff --git a/cinderlib/cinderlib.py b/cinderlib/cinderlib.py index 877dc38..567e6d8 100644 --- a/cinderlib/cinderlib.py +++ b/cinderlib/cinderlib.py @@ -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 + # ') 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): diff --git a/cinderlib/nos_brick.py b/cinderlib/nos_brick.py deleted file mode 100644 index 0e91ca4..0000000 --- a/cinderlib/nos_brick.py +++ /dev/null @@ -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 diff --git a/cinderlib/objects.py b/cinderlib/objects.py index 9427a30..91af352 100644 --- a/cinderlib/objects.py +++ b/cinderlib/objects.py @@ -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 diff --git a/cinderlib/tests/unit/objects/test_connection.py b/cinderlib/tests/unit/objects/test_connection.py index 395c1c9..d550f7c 100644 --- a/cinderlib/tests/unit/objects/test_connection.py +++ b/cinderlib/tests/unit/objects/test_connection.py @@ -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, diff --git a/cinderlib/tests/unit/objects/test_volume.py b/cinderlib/tests/unit/objects/test_volume.py index 27eb272..5e0d069 100644 --- a/cinderlib/tests/unit/objects/test_volume.py +++ b/cinderlib/tests/unit/objects/test_volume.py @@ -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) diff --git a/cinderlib/tests/unit/test_cinderlib.py b/cinderlib/tests/unit/test_cinderlib.py index cd79258..bd0f96a 100644 --- a/cinderlib/tests/unit/test_cinderlib.py +++ b/cinderlib/tests/unit/test_cinderlib.py @@ -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 diff --git a/cinderlib/tests/unit/test_nos_brick.py b/cinderlib/tests/unit/test_nos_brick.py deleted file mode 100644 index 6a526ad..0000000 --- a/cinderlib/tests/unit/test_nos_brick.py +++ /dev/null @@ -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) diff --git a/releasenotes/notes/privsep-support-acbc955b9845a6ef.yaml b/releasenotes/notes/privsep-support-acbc955b9845a6ef.yaml new file mode 100644 index 0000000..bbedc94 --- /dev/null +++ b/releasenotes/notes/privsep-support-acbc955b9845a6ef.yaml @@ -0,0 +1,5 @@ +--- +fixes: + - | + `Bug #1883720 `_: Added + privsep support, increasing cinderlib's compatibility with Cinder drivers. diff --git a/tools/virtualenv-sudo.sh b/tools/virtualenv-sudo.sh index 063fb5c..906f019 100755 --- a/tools/virtualenv-sudo.sh +++ b/tools/virtualenv-sudo.sh @@ -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"