Source code for cinderlib.nos_brick

# 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__)


[docs]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. """
[docs] 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)
[docs] 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)
[docs] 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
[docs] 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 _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))
[docs]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