# (c) Copyright 2014-2016 Hewlett-Packard Development Company, L.P. # # 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. """ Freezer LVM related functions """ import os import re import subprocess from oslo_log import log from oslo_utils import uuidutils from freezer.common import config as freezer_config from freezer.utils import utils LOG = log.getLogger(__name__) def lvm_snap_remove(backup_opt_dict): """ Unmount the snapshot and removes it :param backup_opt_dict.lvm_dirmount: mount point of the snapshot :param backup_opt_dict.lvm_volgroup: volume group to which the lv belongs :param backup_opt_dict.lvm_snapname: name of the snapshot lv :return: None, raises on error """ try: _umount(backup_opt_dict.lvm_dirmount) except Exception as e: LOG.warning("Snapshot unmount errror: {0}".format(e)) lv = os.path.join('/dev', backup_opt_dict.lvm_volgroup, backup_opt_dict.lvm_snapname) _lvremove(lv) LOG.info('Snapshot volume {0} removed'.format(lv)) def get_vol_fs_type(vol_name): """ The argument need to be a full path lvm name i.e. /dev/vg0/var or a disk partition like /dev/sda1. The returnet value is the file system type """ if os.path.exists(vol_name) is False: err = 'Provided volume name not found: {0} '.format(vol_name) LOG.exception(err) raise Exception(err) file_cmd = '{0} -0 -bLs --no-pad --no-buffer --preserve-date \ {1}'.format(utils.find_executable("file"), vol_name) file_process = subprocess.Popen( file_cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True, executable=utils.find_executable("bash")) (file_out, file_err) = file_process.communicate() file_match = re.search(r'(\S+?) filesystem data', file_out, re.I) if file_match is None: err = 'File system type not guessable: {0}'.format(file_err) LOG.exception(err) raise Exception(err) else: filesys_type = file_match.group(1) LOG.info('File system {0} found for volume {1}'.format( filesys_type, vol_name)) return filesys_type.lower().strip() def lvm_snap(backup_opt_dict): """ Checks the provided parameters and create the lvm snapshot if requested The path_to_backup might be adjusted in case the user requested a lvm snapshot without specifying an exact path for the snapshot). The assumption in this case is that the user wants to use the lvm snapshot capability to backup the specified filesystem path, leaving out all the rest of the parameters which will guessed and set by freezer. :param backup_opt_dict: the configuration dict :return: True if the snapshot has been taken, False otherwise """ lvm_uuid = uuidutils.generate_uuid(dashed=False) if not backup_opt_dict.lvm_snapname: backup_opt_dict.lvm_snapname = \ "{0}_{1}".format(freezer_config.DEFAULT_LVM_SNAP_BASENAME, lvm_uuid) # adjust/check lvm parameters according to provided path_to_backup lvm_info = get_lvm_info(backup_opt_dict.path_to_backup) if not backup_opt_dict.lvm_volgroup: backup_opt_dict.lvm_volgroup = lvm_info['volgroup'] if not backup_opt_dict.lvm_srcvol: backup_opt_dict.lvm_srcvol = lvm_info['srcvol'] if not backup_opt_dict.lvm_dirmount: utils.create_dir(freezer_config.DEFAULT_LVM_MOUNT_BASEDIR) backup_opt_dict.lvm_dirmount = \ "{0}/mount_{1}".format(freezer_config.DEFAULT_LVM_MOUNT_BASEDIR, lvm_uuid) if not validate_lvm_params(backup_opt_dict): LOG.info('No LVM requested/configured') return False utils.create_dir(backup_opt_dict.lvm_dirmount) if '%' in backup_opt_dict.lvm_snapsize: lvm_size_option = "--extents" else: lvm_size_option = "--size" lvm_create_command = ( '{0} {1} {2} --snapshot --permission {3} ' '--name {4} {5}'.format( utils.find_executable('lvcreate'), lvm_size_option, backup_opt_dict.lvm_snapsize, ('r' if backup_opt_dict.lvm_snapperm == 'ro' else backup_opt_dict.lvm_snapperm), backup_opt_dict.lvm_snapname, backup_opt_dict.lvm_srcvol)) lvm_process = subprocess.Popen( lvm_create_command, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True, executable=utils.find_executable('bash')) (lvm_out, lvm_err) = lvm_process.communicate() if lvm_process.returncode: raise Exception('lvm snapshot creation error: {0}'.format(lvm_err)) LOG.debug('{0}'.format(lvm_out)) LOG.warning('Logical volume "{0}" created'. format(backup_opt_dict.lvm_snapname)) # Guess the file system of the provided source volume and st mount # options accordingly filesys_type = get_vol_fs_type(backup_opt_dict.lvm_srcvol) mount_options = '-o {}'.format(backup_opt_dict.lvm_snapperm) if 'xfs' == filesys_type: mount_options = ' -onouuid ' # Mount the newly created snapshot to dir_mount abs_snap_name = '/dev/{0}/{1}'.format( backup_opt_dict.lvm_volgroup, backup_opt_dict.lvm_snapname) mount_command = '{0} {1} {2} {3}'.format( utils.find_executable('mount'), mount_options, abs_snap_name, backup_opt_dict.lvm_dirmount) mount_process = subprocess.Popen( mount_command, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True, executable=utils.find_executable('bash')) mount_err = mount_process.communicate()[1] if 'already mounted' in mount_err: LOG.warning('Volume {0} already mounted on {1}\ '.format(abs_snap_name, backup_opt_dict.lvm_dirmount)) return True if mount_err: LOG.error("Snapshot mount error. Removing snapshot") lvm_snap_remove(backup_opt_dict) raise Exception('lvm snapshot mounting error: {0}'.format(mount_err)) else: LOG.warning( 'Volume {0} successfully mounted on {1}'.format( abs_snap_name, backup_opt_dict.lvm_dirmount)) # After snapshot is mounted, adjust path_to_backup according the mount # point and relative path of the snapshot volume. lvm_info = get_lvm_info(backup_opt_dict.path_to_backup) backup_opt_dict.path_to_backup = os.path.join(backup_opt_dict.lvm_dirmount, lvm_info['snap_path']) return True def get_lvm_info(path): """ Take a file system path as argument as backup_opt_dict.path_to_backup and return a list containing lvm_srcvol, lvm_volgroup where the path is mounted on. :param path: the original file system path where backup needs to be executed :returns: a dict containing the keys 'volgroup', 'srcvol' and 'snap_path' """ mount_point_path, snap_path = utils.get_mount_from_path(path) with open('/proc/mounts', 'r') as mount_fd: mount_points = mount_fd.readlines() lvm_volgroup, lvm_srcvol, lvm_device = lvm_guess( mount_point_path, mount_points, '/proc/mounts') if not lvm_device: mount_process = subprocess.Popen( ['mount'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=os.environ) mount_out, mount_err = mount_process.communicate() mount_points = [m for m in mount_out.split('\n') if m] lvm_volgroup, lvm_srcvol, lvm_device = lvm_guess( mount_point_path, mount_points, 'mount') if not lvm_device: raise Exception( 'Cannot find {0} in {1}, please provide volume group and ' 'volume name explicitly'.format(mount_point_path, mount_points)) lvm_params = {'volgroup': lvm_volgroup, 'srcvol': lvm_device, 'snap_path': snap_path} return lvm_params def lvm_guess(mount_point_path, mount_points, source='/proc/mounts'): """Guess lvm vol group and vol name from mount point Extract the vol group and vol name from given list of mount_points and mount_point_path :param mount_point_path: mount path :param mount_points: list of currently mounted devices :return: a list containing volume group, volume name and full device path """ lvm_volgroup = lvm_srcvol = lvm_device = None for mount_line in mount_points: if source == '/proc/mounts': device, mount_path = mount_line.split(' ')[0:2] elif source == 'mount': mount_list = mount_line.split(' ')[0:3] device = mount_list[0] mount_path = mount_list[2] if mount_point_path.strip() == mount_path.strip(): mount_match = re.search( r'/dev/mapper/(\w.+?\w)-(\w.+?\w)$', device) if mount_match: lvm_volgroup = mount_match.group(1).replace('--', '-') lvm_srcvol = mount_match.group(2).replace('--', '-') lvm_device = u'/dev/{0}/{1}'.format(lvm_volgroup, lvm_srcvol) break return lvm_volgroup, lvm_srcvol, lvm_device def validate_lvm_params(backup_opt_dict): """ Validates the parameters and raises in case of missing values :param backup_opt_dict: :return: False is snapshot is not requested, True snapshot is requested and parameters are valid """ if backup_opt_dict.lvm_snapperm not in ('ro', 'rw'): raise ValueError('Error: Invalid value for option lvm-snap-perm: ' '{}'.format(backup_opt_dict.lvm_snapperm)) if not backup_opt_dict.path_to_backup: raise ValueError('Error: no path-to-backup and ' 'no lvm-auto-snap provided') if not backup_opt_dict.lvm_srcvol and not backup_opt_dict.lvm_volgroup: # no lvm parameters provided, assume lvm snapshot is not requested return False if not backup_opt_dict.lvm_srcvol: raise ValueError('Error: no lvm-srcvol and ' 'no lvm-auto-snap provided') if not backup_opt_dict.lvm_volgroup: raise ValueError('Error: no lvm-volgroup and ' 'no lvm-auto-snap provided') LOG.info('Source LVM Volume: {0}'.format( backup_opt_dict.lvm_srcvol)) LOG.info('LVM Volume Group: {0}'.format( backup_opt_dict.lvm_volgroup)) LOG.info('Snapshot name: {0}'.format( backup_opt_dict.lvm_snapname)) LOG.info('Snapshot size: {0}'.format( backup_opt_dict.lvm_snapsize)) LOG.info('Directory where the lvm snaphost will be mounted on:' ' {0}'.format(backup_opt_dict.lvm_dirmount.strip())) LOG.info('Path to backup (including snapshot): {0}' .format(backup_opt_dict.path_to_backup)) return True def _umount(path): # Change dir if we are within the mount point to be removed. if os.getcwd().startswith(os.path.normpath(path)): os.chdir('/') umount_proc = subprocess.Popen('{0} -l -f {1}'.format( utils.find_executable('umount'), path), stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True, executable=utils.find_executable('bash')) (umount_out, mount_err) = umount_proc.communicate() if umount_proc.returncode: raise Exception('Impossible to umount {0}. {1}' .format(path, mount_err)) os.rmdir(path) LOG.info('Volume {0} unmounted'.format(path)) def _lvremove(lv): for attempt in range(5): lvremove_proc = subprocess.Popen( '{0} -f {1}'.format(utils.find_executable('lvremove'), lv), stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True, executable=utils.find_executable('bash')) output, error = lvremove_proc.communicate() if lvremove_proc.returncode: if "contains a filesystem in use" in error: LOG.warning("Couldn't remove volume {0}. " "It is still in use.".format(lv)) log_volume_holding_process(lv) else: break else: return # Raise if five attempts made or different error than fs in use raise Exception('Unable to remove snapshot {0}. {1}'.format(lv, error)) def log_volume_holding_process(lv): try: # Let's try to provide more information on the failure devices = [i.split("\t") for i in subprocess.check_output([ utils.find_executable('dmsetup'), "ls"]).splitlines()] dev_id = [i[1].strip("()").split(":") for i in devices if lv.split("/").pop() in i[0] and not i[0].endswith("cow")][0] command = "{} | grep {},{}".format( # lsof is quite long, so no need to add a sleep here utils.find_executable('lsof'), dev_id[0], dev_id[1]) process = subprocess.check_output([command], shell=True) LOG.warning("Process holding the volume is '{}'".format(process)) except Exception as e: LOG.warning("Could not get informations on the process holding the" " volume: {}".format(str(e)))