# Copyright 2012 NetApp # Copyright 2016 Mirantis 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. """ LVM Driver for shares. """ import math import os import re from oslo_config import cfg from oslo_log import log from oslo_utils import importutils import six from manila import exception from manila.i18n import _ from manila.share import driver from manila.share.drivers import generic from manila.share import utils LOG = log.getLogger(__name__) share_opts = [ cfg.StrOpt('lvm_share_export_root', default='$state_path/mnt', help='Base folder where exported shares are located.'), cfg.StrOpt('lvm_share_export_ip', help='IP to be added to export string.'), cfg.IntOpt('lvm_share_mirrors', default=0, help='If set, create LVMs with multiple mirrors. Note that ' 'this requires lvm_mirrors + 2 PVs with available space.'), cfg.StrOpt('lvm_share_volume_group', default='lvm-shares', help='Name for the VG that will contain exported shares.'), cfg.ListOpt('lvm_share_helpers', default=[ 'CIFS=manila.share.drivers.helpers.CIFSHelperUserAccess', 'NFS=manila.share.drivers.helpers.NFSHelper', ], help='Specify list of share export helpers.'), ] CONF = cfg.CONF CONF.register_opts(share_opts) CONF.register_opts(generic.share_opts) class LVMMixin(driver.ExecuteMixin): def check_for_setup_error(self): """Returns an error if prerequisites aren't met.""" out, err = self._execute('vgs', '--noheadings', '-o', 'name', run_as_root=True) volume_groups = out.split() if self.configuration.lvm_share_volume_group not in volume_groups: msg = (_("share volume group %s doesn't exist") % self.configuration.lvm_share_volume_group) raise exception.InvalidParameterValue(err=msg) if not self.configuration.lvm_share_export_ip: msg = (_("lvm_share_export_ip isn't specified")) raise exception.InvalidParameterValue(err=msg) def _allocate_container(self, share): sizestr = '%sG' % share['size'] cmd = ['lvcreate', '-L', sizestr, '-n', share['name'], self.configuration.lvm_share_volume_group] if self.configuration.lvm_share_mirrors: cmd += ['-m', self.configuration.lvm_share_mirrors, '--nosync'] terras = int(sizestr[:-1]) / 1024.0 if terras >= 1.5: rsize = int(2 ** math.ceil(math.log(terras) / math.log(2))) # NOTE(vish): Next power of two for region size. See: # http://red.ht/U2BPOD cmd += ['-R', six.text_type(rsize)] self._try_execute(*cmd, run_as_root=True) device_name = self._get_local_path(share) self._execute('mkfs.%s' % self.configuration.share_volume_fstype, device_name, run_as_root=True) def _extend_container(self, share, device_name, size): cmd = ['lvextend', '-L', '%sG' % size, '-n', device_name] self._try_execute(*cmd, run_as_root=True) def _deallocate_container(self, share_name): """Deletes a logical volume for share.""" try: self._try_execute('lvremove', '-f', "%s/%s" % (self.configuration.lvm_share_volume_group, share_name), run_as_root=True) except exception.ProcessExecutionError as exc: if "not found" not in exc.stderr: LOG.exception("Error deleting volume") raise LOG.warning("Volume not found: %s" % exc.stderr) def _create_snapshot(self, context, snapshot): """Creates a snapshot.""" orig_lv_name = "%s/%s" % (self.configuration.lvm_share_volume_group, snapshot['share_name']) self._try_execute( 'lvcreate', '-L', '%sG' % snapshot['share']['size'], '--name', snapshot['name'], '--snapshot', orig_lv_name, run_as_root=True) snapshot_device_name = self._get_local_path(snapshot) self._execute( 'tune2fs', '-U', 'random', snapshot_device_name, run_as_root=True, ) def create_snapshot(self, context, snapshot, share_server=None): self._create_snapshot(context, snapshot) def delete_snapshot(self, context, snapshot, share_server=None): """Deletes a snapshot.""" self._deallocate_container(snapshot['name']) class LVMShareDriver(LVMMixin, driver.ShareDriver): """Executes commands relating to Shares.""" def __init__(self, *args, **kwargs): """Do initialization.""" super(LVMShareDriver, self).__init__([False], *args, **kwargs) self.configuration.append_config_values(share_opts) self.configuration.append_config_values(generic.share_opts) self.configuration.share_mount_path = ( self.configuration.lvm_share_export_root) self._helpers = None self.backend_name = self.configuration.safe_get( 'share_backend_name') or 'LVM' # Set of parameters used for compatibility with # Generic driver's helpers. self.share_server = { 'public_address': self.configuration.lvm_share_export_ip, 'instance_id': self.backend_name, 'lock_name': 'manila_lvm', } def _ssh_exec_as_root(self, server, command, check_exit_code=True): kwargs = {} if 'sudo' in command: kwargs['run_as_root'] = True command.remove('sudo') kwargs['check_exit_code'] = check_exit_code return self._execute(*command, **kwargs) def do_setup(self, context): """Any initialization the volume driver does while starting.""" super(LVMShareDriver, self).do_setup(context) self._setup_helpers() def _setup_helpers(self): """Initializes protocol-specific NAS drivers.""" self._helpers = {} for helper_str in self.configuration.lvm_share_helpers: share_proto, _, import_str = helper_str.partition('=') helper = importutils.import_class(import_str) # TODO(rushiagr): better way to handle configuration # instead of just passing to the helper self._helpers[share_proto.upper()] = helper( self._execute, self._ssh_exec_as_root, self.configuration) def _get_local_path(self, share): # The escape characters are expected by the device mapper. escaped_group = ( self.configuration.lvm_share_volume_group.replace('-', '--')) escaped_name = share['name'].replace('-', '--') return "/dev/mapper/%s-%s" % (escaped_group, escaped_name) def _update_share_stats(self): """Retrieve stats info from share volume group.""" data = { 'share_backend_name': self.backend_name, 'storage_protocol': 'NFS_CIFS', 'reserved_percentage': self.configuration.reserved_share_percentage, 'snapshot_support': True, 'create_share_from_snapshot_support': True, 'revert_to_snapshot_support': True, 'mount_snapshot_support': True, 'driver_name': 'LVMShareDriver', 'pools': self.get_share_server_pools() } super(LVMShareDriver, self)._update_share_stats(data) def get_share_server_pools(self, share_server=None): out, err = self._execute('vgs', self.configuration.lvm_share_volume_group, '--rows', '--units', 'g', run_as_root=True) total_size = re.findall("VSize\s[0-9.]+g", out)[0][6:-1] free_size = re.findall("VFree\s[0-9.]+g", out)[0][6:-1] return [{ 'pool_name': 'lvm-single-pool', 'total_capacity_gb': float(total_size), 'free_capacity_gb': float(free_size), 'reserved_percentage': 0, }, ] def create_share(self, context, share, share_server=None): self._allocate_container(share) # create file system device_name = self._get_local_path(share) location = self._get_helper(share).create_exports( self.share_server, share['name']) self._mount_device(share, device_name) return location def create_share_from_snapshot(self, context, share, snapshot, share_server=None): """Is called to create share from snapshot.""" self._allocate_container(share) snapshot_device_name = self._get_local_path(snapshot) share_device_name = self._get_local_path(share) self._execute( 'tune2fs', '-U', 'random', share_device_name, run_as_root=True, ) self._copy_volume( snapshot_device_name, share_device_name, share['size']) location = self._get_helper(share).create_exports( self.share_server, share['name']) self._mount_device(share, share_device_name) return location def delete_share(self, context, share, share_server=None): self._remove_export(context, share) self._delete_share(context, share) self._deallocate_container(share['name']) def _remove_export(self, ctx, share): """Removes an access rules for a share.""" mount_path = self._get_mount_path(share) if os.path.exists(mount_path): # umount, may be busy try: self._execute('umount', '-f', mount_path, run_as_root=True) except exception.ProcessExecutionError as exc: if 'device is busy' in six.text_type(exc): raise exception.ShareBusyException(reason=share['name']) else: LOG.info('Unable to umount: %s', exc) # remove dir try: os.rmdir(mount_path) except OSError: LOG.warning('Unable to delete %s', mount_path) def ensure_share(self, ctx, share, share_server=None): """Ensure that storage are mounted and exported.""" device_name = self._get_local_path(share) self._mount_device(share, device_name) self._get_helper(share).create_exports( self.share_server, share['name'], recreate=True) def _delete_share(self, ctx, share): """Delete a share.""" try: self._get_helper(share).remove_exports( self.share_server, share['name']) except exception.ProcessExecutionError: LOG.warning("Can't remove share %r", share['id']) except exception.InvalidShare as exc: LOG.warning(exc.message) def update_access(self, context, share, access_rules, add_rules, delete_rules, share_server=None): """Update access rules for given share. This driver has two different behaviors according to parameters: 1. Recovery after error - 'access_rules' contains all access_rules, 'add_rules' and 'delete_rules' shall be empty. Previously existing access rules are cleared and then added back according to 'access_rules'. 2. Adding/Deleting of several access rules - 'access_rules' contains all access_rules, 'add_rules' and 'delete_rules' contain rules which should be added/deleted. Rules in 'access_rules' are ignored and only rules from 'add_rules' and 'delete_rules' are applied. :param context: Current context :param share: Share model with share data. :param access_rules: All access rules for given share :param add_rules: Empty List or List of access rules which should be added. access_rules already contains these rules. :param delete_rules: Empty List or List of access rules which should be removed. access_rules doesn't contain these rules. :param share_server: None or Share server model """ self._get_helper(share).update_access(self.share_server, share['name'], access_rules, add_rules=add_rules, delete_rules=delete_rules) def _get_helper(self, share): if share['share_proto'].lower().startswith('nfs'): return self._helpers['NFS'] elif share['share_proto'].lower().startswith('cifs'): return self._helpers['CIFS'] else: raise exception.InvalidShare(reason='Wrong share protocol') def _mount_device(self, share, device_name): """Mount LVM share and ignore if already mounted.""" mount_path = self._get_mount_path(share) self._execute('mkdir', '-p', mount_path) try: self._execute('mount', device_name, mount_path, run_as_root=True, check_exit_code=True) self._execute('chmod', '777', mount_path, run_as_root=True, check_exit_code=True) except exception.ProcessExecutionError: out, err = self._execute('mount', '-l', run_as_root=True) if device_name in out: LOG.warning("%s is already mounted", device_name) else: raise return mount_path def _unmount_device(self, share): mount_path = self._get_mount_path(share) self._execute('umount', mount_path, run_as_root=True) self._execute('rmdir', mount_path, run_as_root=True) def _get_mount_path(self, share): """Returns path where share is mounted.""" return os.path.join(self.configuration.share_mount_path, share['name']) def _copy_volume(self, srcstr, deststr, size_in_g): # Use O_DIRECT to avoid thrashing the system buffer cache extra_flags = ['iflag=direct', 'oflag=direct'] # Check whether O_DIRECT is supported try: self._execute('dd', 'count=0', 'if=%s' % srcstr, 'of=%s' % deststr, *extra_flags, run_as_root=True) except exception.ProcessExecutionError: extra_flags = [] # Perform the copy self._execute('dd', 'if=%s' % srcstr, 'of=%s' % deststr, 'count=%d' % (size_in_g * 1024), 'bs=1M', *extra_flags, run_as_root=True) def extend_share(self, share, new_size, share_server=None): device_name = self._get_local_path(share) self._extend_container(share, device_name, new_size) self._execute('resize2fs', device_name, run_as_root=True) def revert_to_snapshot(self, context, snapshot, access_rules, share_server=None): share = snapshot['share'] # Temporarily remove all access rules self._get_helper(share).update_access(self.share_server, share['name'], [], [], []) # Unmount the filesystem self._remove_export(context, snapshot) # First we merge the snapshot LV and the share LV # This won't actually do anything until the LV is reactivated snap_lv_name = "%s/%s" % (self.configuration.lvm_share_volume_group, snapshot['name']) self._execute('lvconvert', '--merge', snap_lv_name, run_as_root=True) # Unmount the share so we can deactivate it self._unmount_device(share) # Deactivate the share LV share_lv_name = "%s/%s" % (self.configuration.lvm_share_volume_group, share['name']) self._execute('lvchange', '-an', share_lv_name, run_as_root=True) # Reactivate the share LV. This will trigger the merge and delete the # snapshot. self._execute('lvchange', '-ay', share_lv_name, run_as_root=True) # Now recreate the snapshot that was destroyed by the merge self._create_snapshot(context, snapshot) # At this point we can mount the share again device_name = self._get_local_path(share) self._mount_device(share, device_name) device_name = self._get_local_path(snapshot) self._mount_device(snapshot, device_name) # Lastly we add all the access rules back self._get_helper(share).update_access(self.share_server, share['name'], access_rules, [], []) def create_snapshot(self, context, snapshot, share_server=None): self._create_snapshot(context, snapshot) helper = self._get_helper(snapshot['share']) exports = helper.create_exports(self.share_server, snapshot['name']) device_name = self._get_local_path(snapshot) self._mount_device(snapshot, device_name) return {'export_locations': exports} def delete_snapshot(self, context, snapshot, share_server=None): self._remove_export(context, snapshot) super(LVMShareDriver, self).delete_snapshot(context, snapshot, share_server) def snapshot_update_access(self, context, snapshot, access_rules, add_rules, delete_rules, share_server=None): """Update access rules for given snapshot. This driver has two different behaviors according to parameters: 1. Recovery after error - 'access_rules' contains all access_rules, 'add_rules' and 'delete_rules' shall be empty. Previously existing access rules are cleared and then added back according to 'access_rules'. 2. Adding/Deleting of several access rules - 'access_rules' contains all access_rules, 'add_rules' and 'delete_rules' contain rules which should be added/deleted. Rules in 'access_rules' are ignored and only rules from 'add_rules' and 'delete_rules' are applied. :param context: Current context :param snapshot: Snapshot model with snapshot data. :param access_rules: All access rules for given snapshot :param add_rules: Empty List or List of access rules which should be added. access_rules already contains these rules. :param delete_rules: Empty List or List of access rules which should be removed. access_rules doesn't contain these rules. :param share_server: None or Share server model """ helper = self._get_helper(snapshot['share']) access_rules, add_rules, delete_rules = utils.change_rules_to_readonly( access_rules, add_rules, delete_rules) helper.update_access(self.share_server, snapshot['name'], access_rules, add_rules=add_rules, delete_rules=delete_rules)