"Fossies" - the Fresh Open Source Software Archive

Member "manila-11.0.1/manila/share/drivers/lvm.py" (1 Feb 2021, 23439 Bytes) of package /linux/misc/openstack/manila-11.0.1.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: 11.0.0_vs_11.0.1.

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