#!/bin/bash

# Copyright (C) 2009 Chris Procter All rights reserved.
# Copyright (C) 2009-2017 Red Hat, Inc. All rights reserved.
#
# This file is part of LVM2.
#
# This copyrighted material is made available to anyone wishing to use,
# modify, copy, or redistribute it subject to the terms and conditions
# of the GNU General Public License v.2.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
#
# vgimportclone: This script is used to rename the VG and change the associated
#                VG and PV UUIDs (primary application being HW snapshot restore)

# following external commands are used throughout the script
# echo and test are internal in bash at least
RM="rm"
BASENAME="basename"
MKTEMP="mktemp"
READLINK="readlink"
GETOPT="getopt"

# user may override lvm location by setting LVM_BINARY
LVM=${LVM_BINARY:-lvm}

die() {
    code=$1; shift
    echo "Fatal:" "$@" 1>&2
    exit "$code"
}

"$LVM" version >& /dev/null || die 2 "Could not run lvm binary '$LVM'"


function getvgname {
### get a unique vg name
###        $1 = list of exists VGs
###        $2 = the name we want
    VGLIST=$1
    VG=$2
    NEWVG=$3

    BNAME="${NEWVG:-${VG}}"
    NAME=${BNAME}
    I=0

    while [[ "${VGLIST}" =~ :${NAME}: ]]
    do
        I=$(( I + 1 ))
        NAME="${BNAME}$I"
    done
    echo "${NAME}"
}


function checkvalue {
### check return value and error if non zero
    if [ "$1" -ne 0 ]; then
        die "$1" "$2, error: $1"
    fi
}


function usage {
### display usage message
    echo "Usage: ${SCRIPTNAME} [options] PhysicalVolume [PhysicalVolume...]"
    echo "    -n|--basevgname - Base name for the new volume group(s)"
    echo "    -i|--import     - Import any exported volume groups found"
    echo "    -t|--test       - Run in test mode"
    echo "    --quiet         - Suppress output"
    echo "    -v|--verbose    - Set verbose level"
    echo "    -d|--debug      - Set debug level"
    echo "    --version       - Display version information"
    echo "    -h|--help       - Display this help message"
    echo ""
    exit 1
}


function cleanup {
    #set to use old lvm.conf
    LVM_SYSTEM_DIR=$ORIG_LVM_SYS_DIR

    if [ "$KEEP_TMP_LVM_SYSTEM_DIR" -eq 1 ]; then
        echo "${SCRIPTNAME}: LVM_SYSTEM_DIR (${TMP_LVM_SYSTEM_DIR}) must be cleaned up manually."
    else
        "$RM" -rf -- "$TMP_LVM_SYSTEM_DIR"
    fi
}

SCRIPTNAME=$("$BASENAME" "$0")


if [ "$UID" != 0 ] && [ "$EUID" != 0 ]; then
    die 3 "${SCRIPTNAME} must be run as root."
fi

LVM_OPTS=""
TEST_OPT=""
DISKS=""
# for compatibility: using mktemp -t rather than --tmpdir
TMP_LVM_SYSTEM_DIR=$("$MKTEMP" -d -t snap.XXXXXXXX)
KEEP_TMP_LVM_SYSTEM_DIR=0
CHANGES_MADE=0
IMPORT=0
DEBUG=""
VERBOSE=""
VERBOSE_COUNT=0
DEVNO=0

if [ -n "${LVM_SYSTEM_DIR}" ]; then
    export ORIG_LVM_SYS_DIR="${LVM_SYSTEM_DIR}"
fi

trap  cleanup 0 1 2 3 4 5 6 7 8 10 11 12 13 14 15 16 17 18

#####################################################################
### Get and check arguments
#####################################################################
OPTIONS=$("$GETOPT" -o n:dhitv \
    -l basevgname:,debug,help,import,quiet,test,verbose,version \
    -n "${SCRIPTNAME}" -- "$@")
[ $? -ne 0 ] && usage
eval set -- "$OPTIONS"

while true
do
    case $1 in
        -n|--basevgname)
            NEWVG=$2; shift; shift
            ;;
        -i|--import)
            IMPORT=1; shift
            ;;
        -t|--test)
            TEST_OPT="-t"
            shift
            ;;
        --quiet)
            LVM_OPTS="--quiet ${LVM_OPTS}"
            shift
            ;;
        -v|--verbose)
            VERBOSE_COUNT=$(( VERBOSE_COUNT + 1 ))
            if [ -z "$VERBOSE" ]
            then
                VERBOSE="-v"
            else
                VERBOSE="${VERBOSE}v"
            fi
            shift
            ;;
        -d|--debug)
            if [ -z "$DEBUG" ]
            then
                DEBUG="-d"
                set -x
            else
                DEBUG="${DEBUG}d"
            fi
            shift
            ;;
        --version)
            "$LVM" version
            shift
            exit 0
            ;;
        -h|--help)
            usage; shift
            ;;
        --)
            shift; break
            ;;
        *)
            usage
            ;;
    esac
done

# turn on DEBUG (special case associated with -v use)
if [ -z "$DEBUG" ] && [ "$VERBOSE_COUNT" -gt 3 ]; then
    DEBUG="-d"
    set -x
fi

# setup LVM_OPTS
if [ -n "$DEBUG" ] || [ -n "$VERBOSE" ]; then
    LVM_OPTS="${LVM_OPTS} ${DEBUG} ${VERBOSE}"
fi

# process remaining arguments (which should be disks)
for ARG
do
    if [ -b "$ARG" ]
    then
        ln -s "$ARG" "${TMP_LVM_SYSTEM_DIR}/vgimport${DEVNO}"
        DISKS="${DISKS} ${TMP_LVM_SYSTEM_DIR}/vgimport${DEVNO}"
        DEVNO=$(( DEVNO + 1 ))
    else
        die 3 "$ARG is not a block device."
    fi
done

### check we have suitable values for important variables
if [ -z "${DISKS}" ]
then
    usage
fi

#####################################################################
### Get the existing state so we can use it later.
### The list of VG names is saved in this format:
###     :vgname1:vgname2:...:vgnameN:
#####################################################################

OLDVGS=":$("$LVM" vgs ${LVM_OPTS} -o name --noheadings --rows --separator : --config 'log{prefix=""}'):"
checkvalue $? "Current VG names could not be collected without errors"

#####################################################################
### Prepare the temporary lvm environment
#####################################################################

for BLOCK in ${DISKS}
do
    FILTER="\"a|^${BLOCK}$|\", ${FILTER}"
done
export FILTER="filter=[ ${FILTER} \"r|.*|\" ]"

LVMCONF="${TMP_LVM_SYSTEM_DIR}/lvm.conf"

CMD_CONFIG_LINE="devices { \
                   scan = [ \"${TMP_LVM_SYSTEM_DIR}\" ] \
                   cache_dir = \"${TMP_LVM_SYSTEM_DIR}/cache\"
                   global_filter = [ \"a|.*|\" ] \
                   ${FILTER}
                 } \
                 global { \
                   use_lvmetad = 0 \
                 }"

$LVM dumpconfig ${LVM_OPTS} --file "${LVMCONF}" --mergedconfig --config "${CMD_CONFIG_LINE}"

checkvalue $? "Failed to generate ${LVMCONF}"
# Only keep TMP_LVM_SYSTEM_DIR if it contains something worth keeping
[ -n "${DEBUG}" ] && KEEP_TMP_LVM_SYSTEM_DIR=1

### set to use new lvm.conf
export LVM_SYSTEM_DIR=${TMP_LVM_SYSTEM_DIR}

# Check if there are any PVs that don't belong to any VG
# or even if there are disks which are not PVs at all.
NOVGDEVLIST=$("$LVM" pvs -a -o pv_name --select vg_name="" --noheadings)
checkvalue $? "Failed to collect information for PV check"
if [ -n "${NOVGDEVLIST}" ]; then
    FOLLOWLIST=""
    while read -r PVNAME; do
        FOLLOW=$("$READLINK" "$PVNAME")
        FOLLOWLIST="$FOLLOWLIST $FOLLOW"
    done <<< "$(echo "$NOVGDEVLIST")"
    die 8 "Specified devices don't belong to a VG:$FOLLOWLIST"
fi

#####################################################################
### Rename the VG(s) and change the VG and PV UUIDs.
#####################################################################
VGLIST=$("$LVM" vgs -o vg_name,vg_exported,vg_missing_pv_count --noheadings --binary)
checkvalue $? "Failed to collect VG information"

while read -r VGNAME VGEXPORTED VGMISSINGPVCOUNT; do
    if [ "$VGMISSINGPVCOUNT" -gt 0 ]; then
        echo "Volume Group ${VGNAME} has unknown PV(s), skipping."
        echo "- Were all associated PV(s) supplied as arguments?"
        continue
    fi

    if [ "$VGEXPORTED" = "1" ]; then
        if [ "${IMPORT}" -eq 1 ]; then
            "$LVM" vgimport ${LVM_OPTS} ${TEST_OPT} "${VGNAME}"
            checkvalue $? "Volume Group ${VGNAME} could not be imported"
        else
            echo "Volume Group ${VGNAME} exported, skipping."
            continue
        fi
    fi

    "$LVM" pvchange ${LVM_OPTS} ${TEST_OPT} --uuid --config 'global{activation=0}' --select "vg_name=${VGNAME}"
    checkvalue $? "Unable to change all PV uuids in VG ${VGNAME}"

    NEWVGNAME=$(getvgname "$OLDVGS" "$VGNAME" "$NEWVG")

    "$LVM" vgchange ${LVM_OPTS} ${TEST_OPT} --uuid --config 'global{activation=0}' "$VGNAME"
    checkvalue $? "Unable to change VG uuid for ${VGNAME}"

    ## if the name isn't going to get changed dont even try.
    if [ "${VGNAME}" != "${NEWVGNAME}" ]
    then
        "$LVM" vgrename ${LVM_OPTS} ${TEST_OPT} "${VGNAME}" "${NEWVGNAME}"
        checkvalue $? "Unable to rename ${VGNAME} to ${NEWVGNAME}"
    fi

    CHANGES_MADE=1
done <<< "$(echo "$VGLIST")"

#####################################################################
### Restore the old environment
#####################################################################
### set to use old lvm.conf
if [ -z "$ORIG_LVM_SYS_DIR" ]
then
    unset LVM_SYSTEM_DIR
else
    LVM_SYSTEM_DIR=$ORIG_LVM_SYS_DIR
fi

### update the device cache and make sure all
### the device nodes we need are straight
if [ "${CHANGES_MADE}" -eq 1 ]
then
    # get global/use_lvmetad config and if set also notify lvmetad about changes
    # since we were running LVM commands above with use_lvmetad=0
    eval "$("$LVM" dumpconfig ${LVM_OPTS} global/use_lvmetad)"
    if [ "$use_lvmetad" = 1 ]
    then
      echo "Notifying lvmetad about changes since it was disabled temporarily."
      echo "(This resolves any WARNING message about restarting lvmetad that appears above.)"
      LVM_OPTS="${LVM_OPTS} --cache"
    fi

    "$LVM" vgscan ${LVM_OPTS} --mknodes
fi

exit 0
