"Fossies" - the Fresh Open Source Software Archive

Member "manila-8.1.4/manila/share/drivers/lvm.py" (19 Nov 2020, 24324 Bytes) of package /linux/misc/openstack/manila-8.1.4.tar.gz:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) Python source code syntax highlighting (style: standard) with prefixed line numbers. Alternatively you can here view or download the uninterpreted source code file. For more information about "lvm.py" see the Fossies "Dox" file reference documentation and the latest Fossies "Diffs" side-by-side code changes report: 8.1.3_vs_8.1.4.

    1 # Copyright 2012 NetApp
    2 # Copyright 2016 Mirantis Inc.
    3 # All Rights Reserved.
    4 #
    5 #    Licensed under the Apache License, Version 2.0 (the "License"); you may
    6 #    not use this file except in compliance with the License. You may obtain
    7 #    a copy of the License at
    8 #
    9 #         http://www.apache.org/licenses/LICENSE-2.0
   10 #
   11 #    Unless required by applicable law or agreed to in writing, software
   12 #    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
   13 #    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
   14 #    License for the specific language governing permissions and limitations
   15 #    under the License.
   16 """
   17 LVM Driver for shares.
   18 
   19 """
   20 
   21 import ipaddress
   22 import math
   23 import os
   24 import re
   25 
   26 from oslo_config import cfg
   27 from oslo_log import log
   28 from oslo_utils import importutils
   29 from oslo_utils import timeutils
   30 import six
   31 
   32 from manila import exception
   33 from manila.i18n import _
   34 from manila.share import driver
   35 from manila.share.drivers import generic
   36 from manila.share import utils
   37 
   38 LOG = log.getLogger(__name__)
   39 
   40 share_opts = [
   41     cfg.StrOpt('lvm_share_export_root',
   42                default='$state_path/mnt',
   43                help='Base folder where exported shares are located.'),
   44     cfg.StrOpt('lvm_share_export_ip',
   45                deprecated_for_removal=True,
   46                deprecated_reason='Use lvm_share_export_ips instead.',
   47                help='IP to be added to export string.'),
   48     cfg.ListOpt('lvm_share_export_ips',
   49                 help='List of IPs to export shares.'),
   50     cfg.IntOpt('lvm_share_mirrors',
   51                default=0,
   52                help='If set, create LVMs with multiple mirrors. Note that '
   53                     'this requires lvm_mirrors + 2 PVs with available space.'),
   54     cfg.StrOpt('lvm_share_volume_group',
   55                default='lvm-shares',
   56                help='Name for the VG that will contain exported shares.'),
   57     cfg.ListOpt('lvm_share_helpers',
   58                 default=[
   59                     'CIFS=manila.share.drivers.helpers.CIFSHelperUserAccess',
   60                     'NFS=manila.share.drivers.helpers.NFSHelper',
   61                 ],
   62                 help='Specify list of share export helpers.'),
   63 ]
   64 
   65 CONF = cfg.CONF
   66 CONF.register_opts(share_opts)
   67 CONF.register_opts(generic.share_opts)
   68 
   69 
   70 class LVMMixin(driver.ExecuteMixin):
   71     def check_for_setup_error(self):
   72         """Returns an error if prerequisites aren't met."""
   73         out, err = self._execute('vgs', '--noheadings', '-o', 'name',
   74                                  run_as_root=True)
   75         volume_groups = out.split()
   76         if self.configuration.lvm_share_volume_group not in volume_groups:
   77             msg = (_("Share volume group %s doesn't exist.")
   78                    % self.configuration.lvm_share_volume_group)
   79             raise exception.InvalidParameterValue(err=msg)
   80 
   81         if (self.configuration.lvm_share_export_ip and
   82                 self.configuration.lvm_share_export_ips):
   83             msg = (_("Only one of lvm_share_export_ip or lvm_share_export_ips"
   84                      " may be specified."))
   85             raise exception.InvalidParameterValue(err=msg)
   86         if not (self.configuration.lvm_share_export_ip or
   87                 self.configuration.lvm_share_export_ips):
   88             msg = (_("Neither lvm_share_export_ip nor lvm_share_export_ips is"
   89                      " specified."))
   90             raise exception.InvalidParameterValue(err=msg)
   91 
   92     def _allocate_container(self, share):
   93         sizestr = '%sG' % share['size']
   94         cmd = ['lvcreate', '-L', sizestr, '-n', share['name'],
   95                self.configuration.lvm_share_volume_group]
   96         if self.configuration.lvm_share_mirrors:
   97             cmd += ['-m', self.configuration.lvm_share_mirrors, '--nosync']
   98             terras = int(sizestr[:-1]) / 1024.0
   99             if terras >= 1.5:
  100                 rsize = int(2 ** math.ceil(math.log(terras) / math.log(2)))
  101                 # NOTE(vish): Next power of two for region size. See:
  102                 #             http://red.ht/U2BPOD
  103                 cmd += ['-R', six.text_type(rsize)]
  104 
  105         self._try_execute(*cmd, run_as_root=True)
  106         device_name = self._get_local_path(share)
  107         self._execute('mkfs.%s' % self.configuration.share_volume_fstype,
  108                       device_name, run_as_root=True)
  109 
  110     def _extend_container(self, share, device_name, size):
  111         cmd = ['lvextend', '-L', '%sG' % size, '-r', device_name]
  112         self._try_execute(*cmd, run_as_root=True)
  113 
  114     def _deallocate_container(self, share_name):
  115         """Deletes a logical volume for share."""
  116         try:
  117             self._try_execute('lvremove', '-f', "%s/%s" %
  118                               (self.configuration.lvm_share_volume_group,
  119                                share_name), run_as_root=True)
  120         except exception.ProcessExecutionError as exc:
  121             err_pattern = re.compile(".*failed to find.*|.*not found.*",
  122                                      re.IGNORECASE)
  123             if not err_pattern.match(exc.stderr):
  124                 LOG.exception("Error deleting volume")
  125                 raise
  126             LOG.warning("Volume not found: %s", exc.stderr)
  127 
  128     def _create_snapshot(self, context, snapshot):
  129         """Creates a snapshot."""
  130         orig_lv_name = "%s/%s" % (self.configuration.lvm_share_volume_group,
  131                                   snapshot['share_name'])
  132         self._try_execute(
  133             'lvcreate', '-L', '%sG' % snapshot['share']['size'],
  134             '--name', snapshot['name'],
  135             '--snapshot', orig_lv_name, run_as_root=True)
  136 
  137         self._set_random_uuid_to_device(snapshot)
  138 
  139     def _set_random_uuid_to_device(self, share_or_snapshot):
  140         # NOTE(vponomaryov): 'tune2fs' is required to make
  141         # filesystem of share created from snapshot have
  142         # unique ID, in case of LVM volumes, by default,
  143         # it will have the same UUID as source volume. Closes #1645751
  144         # NOTE(gouthamr): Executing tune2fs -U only works on
  145         # a recently checked filesystem.
  146         # See: https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=857336
  147         device_path = self._get_local_path(share_or_snapshot)
  148         self._execute('e2fsck', '-y', '-f', device_path, run_as_root=True)
  149         self._execute(
  150             'tune2fs', '-U', 'random', device_path, run_as_root=True,
  151         )
  152 
  153     def create_snapshot(self, context, snapshot, share_server=None):
  154         self._create_snapshot(context, snapshot)
  155 
  156     def delete_snapshot(self, context, snapshot, share_server=None):
  157         """Deletes a snapshot."""
  158         self._deallocate_container(snapshot['name'])
  159 
  160 
  161 class LVMShareDriver(LVMMixin, driver.ShareDriver):
  162     """Executes commands relating to Shares."""
  163 
  164     def __init__(self, *args, **kwargs):
  165         """Do initialization."""
  166         super(LVMShareDriver, self).__init__([False], *args, **kwargs)
  167         self.configuration.append_config_values(share_opts)
  168         self.configuration.append_config_values(generic.share_opts)
  169         self.configuration.share_mount_path = (
  170             self.configuration.lvm_share_export_root)
  171         self._helpers = None
  172         self.configured_ip_version = None
  173         self.backend_name = self.configuration.safe_get(
  174             'share_backend_name') or 'LVM'
  175         # Set of parameters used for compatibility with
  176         # Generic driver's helpers.
  177         self.share_server = {
  178             'instance_id': self.backend_name,
  179             'lock_name': 'manila_lvm',
  180         }
  181         if self.configuration.lvm_share_export_ip:
  182             self.share_server['public_addresses'] = [
  183                 self.configuration.lvm_share_export_ip]
  184         else:
  185             self.share_server['public_addresses'] = (
  186                 self.configuration.lvm_share_export_ips)
  187         self.ipv6_implemented = True
  188 
  189     def _ssh_exec_as_root(self, server, command, check_exit_code=True):
  190         kwargs = {}
  191         if 'sudo' in command:
  192             kwargs['run_as_root'] = True
  193             command.remove('sudo')
  194         kwargs['check_exit_code'] = check_exit_code
  195         return self._execute(*command, **kwargs)
  196 
  197     def do_setup(self, context):
  198         """Any initialization the volume driver does while starting."""
  199         super(LVMShareDriver, self).do_setup(context)
  200         self._setup_helpers()
  201 
  202     def _setup_helpers(self):
  203         """Initializes protocol-specific NAS drivers."""
  204         self._helpers = {}
  205         for helper_str in self.configuration.lvm_share_helpers:
  206             share_proto, _, import_str = helper_str.partition('=')
  207             helper = importutils.import_class(import_str)
  208             # TODO(rushiagr): better way to handle configuration
  209             #                 instead of just passing to the helper
  210             self._helpers[share_proto.upper()] = helper(
  211                 self._execute, self._ssh_exec_as_root, self.configuration)
  212 
  213     def _get_local_path(self, share):
  214         # The escape characters are expected by the device mapper.
  215         escaped_group = (
  216             self.configuration.lvm_share_volume_group.replace('-', '--'))
  217         escaped_name = share['name'].replace('-', '--')
  218         return "/dev/mapper/%s-%s" % (escaped_group, escaped_name)
  219 
  220     def _update_share_stats(self):
  221         """Retrieve stats info from share volume group."""
  222         data = {
  223             'share_backend_name': self.backend_name,
  224             'storage_protocol': 'NFS_CIFS',
  225             'reserved_percentage':
  226                 self.configuration.reserved_share_percentage,
  227             'snapshot_support': True,
  228             'create_share_from_snapshot_support': True,
  229             'revert_to_snapshot_support': True,
  230             'mount_snapshot_support': True,
  231             'driver_name': 'LVMShareDriver',
  232             'pools': self.get_share_server_pools(),
  233         }
  234         super(LVMShareDriver, self)._update_share_stats(data)
  235 
  236     def get_share_server_pools(self, share_server=None):
  237         out, err = self._execute('vgs',
  238                                  self.configuration.lvm_share_volume_group,
  239                                  '--rows', '--units', 'g',
  240                                  run_as_root=True)
  241         total_size = re.findall("VSize\s[0-9.]+g", out)[0][6:-1]
  242         free_size = re.findall("VFree\s[0-9.]+g", out)[0][6:-1]
  243         return [{
  244             'pool_name': 'lvm-single-pool',
  245             'total_capacity_gb': float(total_size),
  246             'free_capacity_gb': float(free_size),
  247             'reserved_percentage': 0,
  248         }, ]
  249 
  250     def create_share(self, context, share, share_server=None):
  251         self._allocate_container(share)
  252         # create file system
  253         device_name = self._get_local_path(share)
  254         location = self._get_helper(share).create_exports(
  255             self.share_server, share['name'])
  256         self._mount_device(share, device_name)
  257         return location
  258 
  259     def create_share_from_snapshot(self, context, share, snapshot,
  260                                    share_server=None):
  261         """Is called to create share from snapshot."""
  262         self._allocate_container(share)
  263         snapshot_device_name = self._get_local_path(snapshot)
  264         share_device_name = self._get_local_path(share)
  265         self._set_random_uuid_to_device(share)
  266         self._copy_volume(
  267             snapshot_device_name, share_device_name, share['size'])
  268         location = self._get_helper(share).create_exports(
  269             self.share_server, share['name'])
  270         self._mount_device(share, share_device_name)
  271         return location
  272 
  273     def delete_share(self, context, share, share_server=None):
  274         self._unmount_device(share, raise_if_missing=False)
  275         self._delete_share(context, share)
  276         self._deallocate_container(share['name'])
  277 
  278     def _unmount_device(self, share_or_snapshot, raise_if_missing=True):
  279         """Unmount the filesystem of a share or snapshot LV."""
  280         mount_path = self._get_mount_path(share_or_snapshot)
  281         if os.path.exists(mount_path):
  282             # umount, may be busy
  283             try:
  284                 self._execute('umount', '-f', mount_path, run_as_root=True)
  285             except exception.ProcessExecutionError as exc:
  286                 if 'device is busy' in exc.stderr.lower():
  287                     raise exception.ShareBusyException(
  288                         reason=share_or_snapshot['name'])
  289                 elif 'not mounted' in exc.stderr.lower():
  290                     if raise_if_missing:
  291                         LOG.error('Unable to find device: %s', exc)
  292                         raise
  293                 else:
  294                     LOG.error('Unable to umount: %s', exc)
  295                     raise
  296             # remove dir
  297             self._execute('rmdir', mount_path, run_as_root=True)
  298 
  299     def ensure_shares(self, context, shares):
  300         updates = {}
  301         for share in shares:
  302             updates[share['id']] = {
  303                 'export_locations': self.ensure_share(context, share)}
  304         return updates
  305 
  306     def ensure_share(self, ctx, share, share_server=None):
  307         """Ensure that storage are mounted and exported."""
  308         device_name = self._get_local_path(share)
  309         self._mount_device(share, device_name)
  310         return self._get_helper(share).create_exports(
  311             self.share_server, share['name'], recreate=True)
  312 
  313     def _delete_share(self, ctx, share):
  314         """Delete a share."""
  315         try:
  316             self._get_helper(share).remove_exports(
  317                 self.share_server, share['name'])
  318         except exception.ProcessExecutionError:
  319             LOG.warning("Can't remove share %r", share['id'])
  320         except exception.InvalidShare as exc:
  321             LOG.warning(exc)
  322 
  323     def update_access(self, context, share, access_rules, add_rules,
  324                       delete_rules, share_server=None):
  325         """Update access rules for given share.
  326 
  327         This driver has two different behaviors according to parameters:
  328         1. Recovery after error - 'access_rules' contains all access_rules,
  329         'add_rules' and 'delete_rules' shall be empty. Previously existing
  330         access rules are cleared and then added back according
  331         to 'access_rules'.
  332 
  333         2. Adding/Deleting of several access rules - 'access_rules' contains
  334         all access_rules, 'add_rules' and 'delete_rules' contain rules which
  335         should be added/deleted. Rules in 'access_rules' are ignored and
  336         only rules from 'add_rules' and 'delete_rules' are applied.
  337 
  338         :param context: Current context
  339         :param share: Share model with share data.
  340         :param access_rules: All access rules for given share
  341         :param add_rules: Empty List or List of access rules which should be
  342                added. access_rules already contains these rules.
  343         :param delete_rules: Empty List or List of access rules which should be
  344                removed. access_rules doesn't contain these rules.
  345         :param share_server: None or Share server model
  346         """
  347         self._get_helper(share).update_access(self.share_server,
  348                                               share['name'], access_rules,
  349                                               add_rules=add_rules,
  350                                               delete_rules=delete_rules)
  351 
  352     def _get_helper(self, share):
  353         if share['share_proto'].lower().startswith('nfs'):
  354             return self._helpers['NFS']
  355         elif share['share_proto'].lower().startswith('cifs'):
  356             return self._helpers['CIFS']
  357         else:
  358             raise exception.InvalidShare(reason='Wrong share protocol')
  359 
  360     def _mount_device(self, share_or_snapshot, device_name):
  361         """Mount LV for share or snapshot and ignore if already mounted."""
  362         mount_path = self._get_mount_path(share_or_snapshot)
  363         self._execute('mkdir', '-p', mount_path)
  364         try:
  365             self._execute('mount', device_name, mount_path,
  366                           run_as_root=True, check_exit_code=True)
  367             self._execute('chmod', '777', mount_path,
  368                           run_as_root=True, check_exit_code=True)
  369         except exception.ProcessExecutionError:
  370             out, err = self._execute('mount', '-l', run_as_root=True)
  371             if device_name in out:
  372                 LOG.warning("%s is already mounted", device_name)
  373             else:
  374                 raise
  375         return mount_path
  376 
  377     def _get_mount_path(self, share_or_snapshot):
  378         """Returns path where share or snapshot is mounted."""
  379         return os.path.join(self.configuration.share_mount_path,
  380                             share_or_snapshot['name'])
  381 
  382     def _copy_volume(self, srcstr, deststr, size_in_g):
  383         # Use O_DIRECT to avoid thrashing the system buffer cache
  384         extra_flags = ['iflag=direct', 'oflag=direct']
  385 
  386         # Check whether O_DIRECT is supported
  387         try:
  388             self._execute('dd', 'count=0', 'if=%s' % srcstr, 'of=%s' % deststr,
  389                           *extra_flags, run_as_root=True)
  390         except exception.ProcessExecutionError:
  391             extra_flags = []
  392 
  393         # Perform the copy
  394         self._execute('dd', 'if=%s' % srcstr, 'of=%s' % deststr,
  395                       'count=%d' % (size_in_g * 1024), 'bs=1M',
  396                       *extra_flags, run_as_root=True)
  397 
  398     def extend_share(self, share, new_size, share_server=None):
  399         device_name = self._get_local_path(share)
  400         self._extend_container(share, device_name, new_size)
  401 
  402     def revert_to_snapshot(self, context, snapshot, share_access_rules,
  403                            snapshot_access_rules, share_server=None):
  404         share = snapshot['share']
  405         # Temporarily remove all access rules
  406         self._get_helper(share).update_access(self.share_server,
  407                                               snapshot['name'], [], [], [])
  408         self._get_helper(share).update_access(self.share_server,
  409                                               share['name'], [], [], [])
  410         # Unmount the snapshot filesystem
  411         self._unmount_device(snapshot)
  412         # Unmount the share filesystem
  413         self._unmount_device(share)
  414         # Merge the snapshot LV back into the share, reverting it
  415         snap_lv_name = "%s/%s" % (self.configuration.lvm_share_volume_group,
  416                                   snapshot['name'])
  417         self._execute('lvconvert', '--merge', snap_lv_name, run_as_root=True)
  418 
  419         # Now recreate the snapshot that was destroyed by the merge
  420         self._create_snapshot(context, snapshot)
  421         # At this point we can mount the share again
  422         device_name = self._get_local_path(share)
  423         self._mount_device(share, device_name)
  424         # Also remount the snapshot
  425         device_name = self._get_local_path(snapshot)
  426         self._mount_device(snapshot, device_name)
  427         # Lastly we add all the access rules back
  428         self._get_helper(share).update_access(self.share_server,
  429                                               share['name'],
  430                                               share_access_rules,
  431                                               [], [])
  432         snapshot_access_rules, __, __ = utils.change_rules_to_readonly(
  433             snapshot_access_rules, [], [])
  434         self._get_helper(share).update_access(self.share_server,
  435                                               snapshot['name'],
  436                                               snapshot_access_rules,
  437                                               [], [])
  438 
  439     def create_snapshot(self, context, snapshot, share_server=None):
  440         self._create_snapshot(context, snapshot)
  441 
  442         device_name = self._get_local_path(snapshot)
  443         self._mount_device(snapshot, device_name)
  444 
  445         helper = self._get_helper(snapshot['share'])
  446         exports = helper.create_exports(self.share_server, snapshot['name'])
  447 
  448         return {'export_locations': exports}
  449 
  450     def delete_snapshot(self, context, snapshot, share_server=None):
  451         self._unmount_device(snapshot, raise_if_missing=False)
  452 
  453         super(LVMShareDriver, self).delete_snapshot(context, snapshot,
  454                                                     share_server)
  455 
  456     def get_configured_ip_versions(self):
  457         if self.configured_ip_version is None:
  458             try:
  459                 self.configured_ip_version = []
  460                 if self.configuration.lvm_share_export_ip:
  461                     self.configured_ip_version.append(ipaddress.ip_address(
  462                         six.text_type(
  463                             self.configuration.lvm_share_export_ip)).version)
  464                 else:
  465                     for ip in self.configuration.lvm_share_export_ips:
  466                         self.configured_ip_version.append(
  467                             ipaddress.ip_address(six.text_type(ip)).version)
  468             except Exception:
  469                 if self.configuration.lvm_share_export_ip:
  470                     message = (_("Invalid 'lvm_share_export_ip' option "
  471                                  "supplied %s.") %
  472                                self.configuration.lvm_share_export_ip)
  473                 else:
  474                     message = (_("Invalid 'lvm_share_export_ips' option "
  475                                  "supplied %s.") %
  476                                self.configuration.lvm_share_export_ips)
  477                 raise exception.InvalidInput(reason=message)
  478         return self.configured_ip_version
  479 
  480     def snapshot_update_access(self, context, snapshot, access_rules,
  481                                add_rules, delete_rules, share_server=None):
  482         """Update access rules for given snapshot.
  483 
  484         This driver has two different behaviors according to parameters:
  485         1. Recovery after error - 'access_rules' contains all access_rules,
  486         'add_rules' and 'delete_rules' shall be empty. Previously existing
  487         access rules are cleared and then added back according
  488         to 'access_rules'.
  489 
  490         2. Adding/Deleting of several access rules - 'access_rules' contains
  491         all access_rules, 'add_rules' and 'delete_rules' contain rules which
  492         should be added/deleted. Rules in 'access_rules' are ignored and
  493         only rules from 'add_rules' and 'delete_rules' are applied.
  494 
  495         :param context: Current context
  496         :param snapshot: Snapshot model with snapshot data.
  497         :param access_rules: All access rules for given snapshot
  498         :param add_rules: Empty List or List of access rules which should be
  499                added. access_rules already contains these rules.
  500         :param delete_rules: Empty List or List of access rules which should be
  501                removed. access_rules doesn't contain these rules.
  502         :param share_server: None or Share server model
  503         """
  504         helper = self._get_helper(snapshot['share'])
  505         access_rules, add_rules, delete_rules = utils.change_rules_to_readonly(
  506             access_rules, add_rules, delete_rules)
  507 
  508         helper.update_access(self.share_server,
  509                              snapshot['name'], access_rules,
  510                              add_rules=add_rules, delete_rules=delete_rules)
  511 
  512     def update_share_usage_size(self, context, shares):
  513         updated_shares = []
  514         out, err = self._execute(
  515             'df', '-l', '--output=target,used',
  516             '--block-size=g')
  517         gathered_at = timeutils.utcnow()
  518 
  519         for share in shares:
  520             try:
  521                 mount_path = self._get_mount_path(share)
  522                 if os.path.exists(mount_path):
  523                     used_size = (re.findall(
  524                         mount_path + "\s*[0-9.]+G", out)[0].
  525                         split(' ')[-1][:-1])
  526                     updated_shares.append({'id': share['id'],
  527                                            'used_size': used_size,
  528                                            'gathered_at': gathered_at})
  529                 else:
  530                     raise exception.NotFound(
  531                         _("Share mount path %s could not be "
  532                           "found.") % mount_path)
  533             except Exception:
  534                 LOG.exception("Failed to gather 'used_size' for share %s.",
  535                               share['id'])
  536 
  537         return updated_shares
  538 
  539     def get_backend_info(self, context):
  540         return {
  541             'export_ips': ','.join(self.share_server['public_addresses']),
  542             'db_version': utils.get_recent_db_migration_id(),
  543         }