#!/usr/bin/perl
#
# (C)opyright 2001 Sistina Software
#
# LVM 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 2, or (at your option)
# any later version.
# 
# LVM 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 LVM; see the file COPYING.  If not, write to
# the Free Software Foundation, 59 Temple Place - Suite 330,
# Boston, MA 02111-1307, USA. 
#
#
# Description:
#
# Perl script to up-/downgrade the VGDA version to 2/1.
#
# Finds all PVs or works on given ones and retrieves the actual sector offset
# to the first PE using pvdisplay. Stores that offset into new PV structure
# member data_location.base and sets the version field in the PV structure to 2.
# Downgrade to version 1 is supported as well.
#
# Volume groups need to be *active*.
#
# It creates a dummy logical volume on empty PVs of a VG in order to retrieve
# the # PE offset using pvdisplay of a VG which will be removed automatically.
#
# DON'T RUN ANY LVM COMMANDS BY HAND *BEFORE* THE UP-/DOWNGRADE OF THE
# LVM SOFTWARE OR THE RESULTS OF THIS TOOLS RUN WILL BE OVERWRITTEN,
# BECAUSE SUCH COMMANDS UPDATE THE VGDA FROM THE LVMTAB!!!
#
# Warning: this program strictly relies on the V1/V2 VGDA layout of the
#          PV structure. Don't change the PV structure layout without
#          changing the version member to > 2 as well!!!
#
#

#
# Changelog
#
#   11/07/2001 - first drop (HM)
#   16/07/2001 - fix to support empty PVs in a VG
#   17/07/2001 - automatic removal of the temporary dummy LVs
#              - added check to update_version() for non
#                supported structure versions
#   18/07/2001 - removed "-a" option because it won't work after a version
#                up-/downgrade
#


# who am i?
$cmd=$0;
$cmd=~s!^.*/!!;

# PV structure V1/V2 is less than a 512 byte sector in size
$PV_LENGTH=512;

# conversion format to unpack/pack the binary PV structure
$PV_FORMAT="a2va456V2a*";


#
# Subroutines
#

# Help
sub usage
{
   my $OUT="STDOUT", $level=$_[0];
   if($level != 0) {$OUT="STDERR";}
   print($OUT "$cmd -- Physical Volume Version Upgrade\n\n",
         "Synopsis:\n",
         "---------\n\n",
         "$cmd\n",
         "\t{-version/-1/-2}\n",
         "\tPhysicalVolumePath [PhysicalVolumePath...]\n\n",
         "WARNING:\n\n",
         "\tYou *need* to have a valid VGDA backup (see vgcfgbackup(8)) \n",
         "\tbefore running this tool!!!\n\n");
   exit($level);
}

# Get some PE number, its sector address and the PE size from pvdisplay
# and calculate the offset (in 512 byte sectors) of the first PE
sub pv_display
{
   my $pvname=$_[0], $vgname="", $pe=-1, $pesize=0, $sector=0;
   
   open(PIPE, "pvdisplay -v $pvname 2>&1|head -300|") || do {
      warn "$cmd -- error opening pipe to pvdisplay\n\n";
      return (-1, "");
   };
   $_=<PIPE>;
   if(/ERROR/) {
      close(PIPE);
      return (-2, "");
   }
   if(/no physical volume ident/i) {
      close(PIPE);
      return (-3, "");
   }
   if(/new physical volume/i) {
      close(PIPE);
      return (-4, "");
   }

   $ext_found=0;
   foreach(<PIPE>) {
      if(/^vg name\s+(\S+)/i) {$vgname=$1; next;}
      if(/^pe size.* (\d+)/i) {$pesize=$1; next;}
      if(!/physical extents/i && $ext_found == 0) {next;}
      $ext_found=1;

      # special case for short LV names
      /^\s+(\d+)\s+\S+\s+\d+\s+(\d+)/ && do {
         $pe=$1;
         $sector=$2;
         last;
      };
      # 2 special cases for long LV names (line break between)
      /^\s+\d+\s+(\d+)/ && do {
         $sector=$1;
         last;
      };
      /^\s+(\d+)\s+\S+\s*/ && do {
         $pe=$1;
         next;
      };
   }

   close(PIPE);
   $pe == -1 && (return -5, $vgname);
   return ($sector - ($pe * $pesize / 2), $vgname);
}

# Read the PV structure form a device and check,
# if it has a valid LVM identifier
sub pv_read
{
   my $pvname=$_[0], $pv="";

   open(PV, $pvname) || do {
      warn "$cmd -- error $! opening \"$pvname\" for read\n\n";
      return -1;
   };
   if(sysread(PV, $pv, $PV_LENGTH, 0) != $PV_LENGTH) {
      warn "$cmd -- error $! reading PV structure from $pvname\n\n";
      close(PV);
      return -2;
   }
   close(PV);
   substr($pv,0,2) ne "HM" && do {
      warn "$cmd -- physical volume \"$pvname\" has an invalid identifier\n\n";
      return -3;
   };
   return $pv;
}

# Write a given PV structure back to a device
#
# Needs to be called with an already checked device because no further checks
# if a valid PV is supposed to be written take place here!
sub pv_write
{
   my $pvname=$_[0], $pv=$_[1];

   open(PV, ">$pvname") || do {
      warn "$cmd -- error $! opening \"$pvname\" for write\n\n";
      return -1;
   };
   if(syswrite(PV, $pv, $PV_LENGTH, 0) != $PV_LENGTH) {
      warn "$cmd -- error $! writing PV structure to \"$pvname\"\n\n";
      close(PV);
      return -2;
   }
   close(PV);
   return 0;
}

# Update the structure version (1 or 2) and fill in
# the data_location.base field
sub update_version
{
   my $new_version=abs(shift @_),
      $pvname=shift @_,
      $offset=shift @_,
      @pv=@_,
      $pvn="",
      $version=$pv[1];

   # Don't change a PV with a version != 1 or 2
   if($version != 1 && $version != 2) {
      warn "$cmd -- physical volume \"$pvname\" has non supported version ",
           "$version\n";
      return -1;
   }
   if($version == $new_version) {
      warn "$cmd -- physical volume \"$pvname\" already has version $version\n";
      return -2;
   }

   # Set new data_location.base content
   $pv[1]=$new_version;
   $pv[4]=$offset;
   $pvn=pack($PV_FORMAT,@pv);

   $ret=&pv_write($pvname, $pvn);
   $ret < 0 && do {
       warn "$cmd -- error writing new PV structure to \"$pvname\"\n\n";
       return -3;
   };
   print "$cmd -- changed to version $new_version on ",
         "physical volume \"$pvname\"\n";
   return 0;
}

# create a temporary dummy LV with 1 PE on an empty LV belonging to some VG
# in order to be able to call pv_display() again on it
sub lv_create_dummy()
{
   my $vgname=$_[0], $pvname=$_[1], $lvname=$_[2];

   open(PIPE, "lvcreate -An -l1 -n$lvname $vgname $pvname 2>&1|") || return -1;
   $lvname="";
   foreach(<PIPE>) {
      /volume "(\S+)" successfully/ && do {
         $lvname=$1;
         last;
      };
   }
   close(PIPE);
   return $lvname;
}

# remove all temporary dummy LVs
sub lv_remove_dummies
{
    scalar @_ < 1 && return;

    foreach(@_) {
       system("lvremove -f $_ >/dev/null 2>&1");
    }
}

#
# Main
#
$what=shift @ARGV;

# check command line options
$what eq "-h" && usage(0);
if($what ne "-1" && $what ne "-2" && $what ne "-version") {usage(1);}
if(substr($ARGV[0], 0, 1) eq "-") {usage(1);}

# other command line arguments need to be PV specials
@pvs=@ARGV;

# do we have some?
scalar(@pvs) < 1 && do {
   warn "$cmd -- no physical volumes found/given\n\n";
   &usage(1);
};

# hash indexed by PV names for offsets to first PE
%pvs=();

# array for optional temporary dummy LVs
@lvs=();

# we gonna get them all
foreach(@pvs) {
   $pvname=$_;
   
   # Downgrades to version 1 don't need to use pvdisplay
   if($what eq "-1") {
      $pvs{$pvname}=0;
   }
   # upgrades to version 2 and general version checks do
   else {
      # try to get the offset in sectors on the PV of the first PE
      # Remark: because pvdisplay(8) is run on *every* PV, we can assume,
      #         that we are dealing with a consistency checked PV
      ($offset, $vgname)=&pv_display($pvname);
      if($offset < 1) {
         if($offset == -2) {
            $offset = 0;
         } elsif ($offset == -3) {
            warn "$cmd -- no physical volume identifier on \"$pvname\"\n";
            next;
         } elsif ($offset == -4) {
            warn "$cmd -- \"$pvname\" is a unused physical volume\n";
            next;
         # we need a dummy LV on this PV in order to retrieve the offset
         # to the first PE
         } elsif ($offset == -5) {
            $t=$pvname; $t=~s!^.*/!!; $lvname=$cmd.$t.$$;
            if(($lvname=&lv_create_dummy($vgname, $pvname, $lvname)) eq "") {
              warn "$cmd -- ERROR creating dummy temporary LV on \"$pvname\"\n";
              next;
            }
            print "$cmd -- created dummy LV \"$lvname\" on empty PV ",
                  "\"$pvname\"\n";
            # save the LV name for later removal
            push (@lvs, $lvname);
            ($offset, $vgname)=&pv_display($pvname);
            if($offset < 1) {
              warn "$cmd -- ERROR: pv_display() failed again\n",
                    "$cmd -- giving up on \"$pvname\"\n";
              next;
            }
         }
      };
      $pvs{$pvname}=$offset;
   }
}

# remove all temporary dummy LVs; this must be doen *before*
# all pv_read()/change/pv_write() runs because otherwise the VGDA would
# be recreated form the lvmtab by running lvremove(8)
&lv_remove_dummies(@lvs);

# go for the PV structure update on all PVs now
foreach(keys %pvs) {
   $pvname=$_;
   $offset=$pvs{$pvname};

   # read the PV structure
   $pv=&pv_read($pvname);
   $pv < 0 && do {
      warn "$cmd -- unable to read physical volume \"$pvname\"\n\n";
      next;
   };

   # split the PV structure into an array
   @pv=unpack($PV_FORMAT, $pv);

   # version update required?
   if($what eq "-1" || $what eq "-2") {
      &update_version($what,$pvname,$offset,@pv);
   # version display required?
   } elsif($what eq "-version") {
      ($id, $version, $middle, $pe_allocated, $data_location, $end)=@pv;
      print("$cmd -- physical volume \"$pvname\" has version: $version / ",
            "data_location: $data_location\n");
   }
}

print "\n";
exit(0);
