#! /bin/bash

# Copyright 2003, 2007  Alexandre Oliva  <aoliva@redhat.com>
# version 0.1.2.3.4.5

# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.

# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# General Public License for more details.

# You should have received a copy of the GNU General Public License
# along with this program; if not, see <http://www.gnu.org/licenses/>.

# This script reorganizes logical extents in a volume group so as to
# comply with the extent locations specified in the input file(s).  It
# is useful after you pvmove the contents of a disk, replace it, and
# want to get back to what you had before.

# The input file(s) may be listed in the command line.  If absent, the
# directions will be read from standard input.  Each line of the input
# should look like this (minus the #s)

# LV:LE PV:PE

# where LV stands for the full device name of a logical extend, LE is
# the logical extent number within this logical volume, PV is a
# physical volume of the volume group that holds LV, and PE is the
# physical extent in which LV:LE should be located at the end of the
# operation.

# If a physical extent listed are already in use by a logical extent
# that is not listed, the logical extent will be moved to a random
# location within the volume group.

# There must be at least one free physical extent in the volume group
# for any reorganization to take place.

# Each logical extent will be moved in general only once, directly to
# the physical extent that should hold it.  Moves will be reordered to
# make this possible.  If reordering is impossible because of cycles,
# one of the logical extents of will be chosen and moved to a free
# block.

# It is safe to interrupt this operation as much as it is safe to
# interrupt pvmove.  If it is restarted, it picks up from where it
# left.  If you do other LV operations in the volume while it is being
# reorganized, such as creating a LV using a block that was initially
# determined to be free, pvmove may fail, and then the script will
# terminate.  It can be then restarted to complete the work.

# This script is provided in the hope that it is useful.  It was
# tested up to a point by myself, but it comes with NO WARRANTIES.
# Back up your data before using it, and don't tell me you were not
# warned.  If it breaks, you get to keep all the pieces.

# Here's roughly what I did to reorganize my 2-disk LVM so as to
# `stripe' the LVs such that consecutive LEs were placed in different
# disks:

# { seq 0 1 `expr 1536 / 2 - 1` | while read f; do
#     echo /dev/all/root:`expr $f \* 2` /dev/hda1:$f;
#     echo /dev/all/root:`expr $f \* 2 + 1` /dev/hde1:$f;
#   done; 
#   seq 0 1 `expr 19804 / 2 - 1` | while read f; do
#     echo /dev/all/home:`expr $f \* 2` /dev/hda1:`expr 768 + $f`;
#     echo /dev/all/home:`expr $f \* 2 + 1` /dev/hde1:`expr 768 + $f`;
#   done;
# } | lvreorg

: ${PVMOVE=pvmove}

set -e

tmpdir=`mktemp -d ${TMPDIR=/tmp}/lvreorg.XXXXXX`
trap "rm -rf $tmpdir; exit $?; exit" 0 1 2 15

LANG=C; export LANG
LC_COLLATE=C; export LC_COLLATE

target=$tmpdir/target
current=$tmpdir/current
free=$tmpdir/free
moves=$tmpdir/moves
unpairable=$tmpdir/unpairable
pvlist=$tmpdir/pvlist

cat ${1+"$@"} | sort -k 1,1 > "$target"

# Sanity check the input file
sort -u -k 1,1 "$target" > "$target".uniq
if diff -U1 "$target".uniq "$target" > "$target".diff; then
  :
else
  echo Input contains more than one entry for the same logical extent: >&2
  cat "$target".diff >&2
  exit 1
fi

sort -k 2,2 "$target" > "$target".sort
sort -u -k 2,2 "$target".sort > "$target".uniq
if diff -U1 "$target".uniq "$target".sort > "$target".diff; then
  :
else
  echo Input contains more than one entry for the same physical extent: >&2
  cat "$target".diff >&2
  exit 1
fi

rm -f "$target".sort "$target".uniq "$target".diff

lvs=`cut -f1 -d: < "$target" | sort -u`

vg=
for lv in $lvs; do
  thisvg=`dirname "$lv"`
  if test -z "$vg"; then
    vg=$thisvg
  elif test "$vg" != "$thisvg"; then
    echo $lv does not belong to $vg >&2
    exit 1
  fi
done

pvs=`vgdisplay -v $vg | awk '/^PV Name/ { print $4; }' | sort -r`
echo "$pvs" | sort > $pvlist

pvdisplay -v $pvs |
  awk '
BEGIN { in_pes = 0; first_free = -1; }
/PV Name/ { pv = $3; in_pes = 0; next; }
/^Total PE/ { total_pe = $3; }
/no logical volume on physical volume/ {
  first_free = 0;
  while (++first_free <= total_pe)
    print "free "pv":"first_free;
  first_free = -1;
  next; }
! in_pes && /--- Physical extents ---/ { in_pes = 1; next; }
in_pes && /^$/ { in_pes = 0; }
in_pes && /PE/ { next; }
in_pes { sub(/^0*/, "", $1); if ($1 == "") $1 = "0"; }
in_pes && $2 == "free" && first_free == -1 {
  print "free "pv":"$1; first_free = $1; next; }
in_pes && $1 == "....." { next; }
in_pes && $2 == "free" && first_free != -1 {
  while (++first_free <= $1)
    print "free "pv":"first_free;
  first_free = -1;
  next; }
{ first_free = -1; }
in_pes { sub(/^0*/, "", $3); if ($3 == "") $3 = "0"; }
in_pes { print $2":"$3" "pv":"$1; }
{ next; }
' |
sort > "$current"

join -v 1 "$target" "$current" > "$unpairable"
if grep . "$unpairable" > /dev/null; then
  echo These logical extents do not exist in their logical volumes: >&2
  cat "$unpairable" >&2
  exit 1
fi

awk '{print $2}' "$target" | cut -f1 -d: | sort -u |
join -v 1 - "$pvlist" > "$unpairable"
if grep . "$unpairable" > /dev/null; then
  echo These physical extents are not members of the volume group: >&2
  cat "$unpairable" >&2
  exit 1
fi

join "$current" "$target" | awk '$2 == $3 { next; } { print; }' |
sort -k 3,3 > "$moves"

if sed 1q "$moves" | grep . > /dev/null; then
  :
else
  echo Nothing to move >&2
  exit 0
fi

sed -n 's,^free ,,p' < "$current" | sort > "$free"

if sed 1q "$free" | grep . > /dev/null; then
  :
else
  echo There are no free extents >&2
  exit 1
fi

rm -f "$current" "$target" "$unpairable" "$pvlist"

while sed 1q "$moves" | grep . > /dev/null; do
  join -1 3 -2 1 -o "1.1 1.2 1.3" "$moves" "$free" > "$moves".now
  if sed 1q "$moves".now | grep . > /dev/null; then
    echo Moving `wc -l "$moves".now | awk '{print $1}'` extents in this round... >&2
    awk '{print $2}' "$moves".now | sort > "$free".new
    join -v 1 -1 3 -2 3 -o "1.1 1.2 1.3" "$moves" "$moves".now > "$moves".new
  else
    sort -k 2,2 "$moves" |
      join -v 2 -1 2 -2 3 -o "2.1 2.2 2.3" - "$moves" > "$moves".new
    newcnt=`wc -l "$moves".new "$free" | awk '{print $1}' | sort -n | sed 1q`
    
    if test "$newcnt" != 0; then
      echo Making room with $count extents from other LVs... >&2
      paste -d " " "$moves".new "$free" | head -$newcnt |
        awk '{print "?:?", $3, $4}' > "$moves".now
      { head -$newcnt "$moves".new | awk '{print $3}';
        tail +`expr $newcnt + 1` "$free"; } | sort > "$free".new
      cp "$moves" "$moves".new
    else
      join -v 1 -o "1.1 1.2 1.3" -1 3 -2 1 "$moves" "$free" |
        head -1 > "$moves".now
      echo Making room with 1 extent from internal cycle... >&2
      head -1 "$free" > "$free".tmp
      head -1 "$moves" | paste -d" " - "$free".tmp |
        awk '{print $1, $2, $4}' > "$moves".now
      { head -1 "$moves" | cut -f2 -d" "; 
        tail +2 "$free"; } | sort > "$free".new
      { tail +2 "$moves";
        head -1 "$moves" | paste -d" " - "$free".tmp |
        awk '{print $1, $4, $3}'; } | sort -k 3,3 > "$moves".new
      rm -f "$free".tmp
    fi
  fi

  while read lv from to; do
    echo ${PVMOVE-pvmove} -f "$from" "$to # $lv"
    ${PVMOVE-pvmove} -f "$from" "$to" > /dev/null || exit
  done < "$moves".now || exit

  mv "$moves".new "$moves"
  mv "$free".new "$free"
done

exit 0
