#!/usr/bin/perl
# Script for unlinking a vs from one skel, and linking it to another.
# (RedHat GNU/Linux version).
# Copyright (c) 1999, 2000 Idaya Ltd.
#
# This file is part of the Virtual Server Administrator (FreeVSD)
#
# FreeVSD 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.
#
# FreeVSD 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 FreeVSD; see the file COPYING.  If not, write to
# the Free Software Foundation, Inc., 59 Temple Place - Suite 330,
# Boston, MA 02111-1307, USA.

# Command syntax:
#	vsd-linkvs.pl [vs] [skel]
#
#	vs - name of the virtual server to link (wildcards are accepted)
#	skel - the full path of the skel to link the vs to
#

use File::Compare;
use DirHandle;
use Getopt::Long;
use VSD;
use VSD::Config;
use strict;

# Obtain all configuration information:
my $vsdconf = VSD::Config->new(file=>$path{vsdconf});
my $sbindir = $VSD::path{vsdsbindir};

# FIXME: default skel_path defined here, should be somewhere more global (VSD.pm?)
my $skel_path = "/skel/";

# Vars used:
my (@vsargs, @vsoptions) = (); # Virtual server array
my (@skelargs, @skeloptions); # Arrays to contain skel information from command line
my ($skelflag, $vsflag) = 0; # Flag to check that we only have 1 skel name on the command 
                             # line and that vs has been found
my ($newskel); # String to contain the new skel name
my ($i); # Integer counter for argument loops
my ($targetvs); # Strings used in foreach loops to specify each vs
my (@vstolink, @linkedvs) = (); # Array containing list of vs to link, & vs that were linked
my ($currentskel, $partition, $mountpoint); # Strings containing current vs information
my ($failcount) = 0; # Integer to count the number (if any) of vs's that failed to link

# Get number of arguments before getopt gets its hand on it
my $num_of_args = @ARGV;

# Hash to contain the arguments
my %options = ( virtualserver => \@vsoptions,
                skel          => \@skeloptions
              );

my $return = &GetOptions (\%options, "virtualserver=s", "skel=s");

# Split up any ',' separated lists
foreach (@vsoptions){
  push (@vsargs, split (',', $_));
}
foreach (@skeloptions){
  push (@skelargs, split (',', $_));
}

if ((@ARGV != 0) || ($return == 0) || ($num_of_args == 0) || (@skelargs != 1)){
  if (@ARGV != 0){
    printf ("$0: error: unknown option: @ARGV\n");
  }
  if (@skelargs > 1){
    printf ("$0: error: multiple skels detected on the command line\n");
  }
  usage ();
}

$newskel = $skelargs[0];

# Is vs valid? / Set up list of vs (expand wilcards)
# obtain a list of all of the vs
my %virtualservers = $vsdconf->getval ( 'VirtualServer' );
my @vsnames = keys %virtualservers;

# Check that vs supplied are valid. If not, notify.
foreach $targetvs (@vsargs){
  $vsflag = 0;
  foreach (@vsnames){
    my $availvs = $_;
    if ($availvs =~ /^$targetvs$/){
      push (@vstolink, $availvs);
      $vsflag++;
    }
  }
  if ($vsflag == 0){
    printf ("$0: error: virtual server: %s not found\n", $targetvs);
  }
}

# For each of the vs that is to be linked:
my (@pid); # Array to contain the childrens pid num.
my ($pidc) = 0; # Process id counter
my ($j) = 0; # for loop counter

foreach (@vstolink){
  my $vserver = $_;
  my (@new_dirs, @rm_dirs,@new_slinked_dirs) = ();
  my ($file, $hstserver);

  # Obtain the name of the vs's hosting server:
  if (! defined ($hstserver = $virtualservers{$vserver}->{HostingServer})){
    printf ("$0: error: Could not obtain the name of the for %s\n", $vserver);
    next;
  }

  # Check that we're not trying to link a skel to the hosting server.
  if ($hstserver eq $vserver){
    printf ("$0: error: Tried to link the VSD Server to a skel\n");
    next;
  }

  # Obtain the name of the current skel
  if (! defined ($currentskel = $virtualservers{$vserver}->{Skel})){
    printf ("$0: error: Could not obtain the current skel for %s\n", $vserver); 
    next;
  }

  # Obtain the partition of the current skel
  if (! defined ($partition = $virtualservers{$vserver}->{Partition})){
    printf ("$0: error: Could not obtain the partition number for %s\n", $vserver);
    next;
  } 

  # Obtain the mount point of the partition
  if (! defined ($mountpoint = $vsdconf->getval ('Partition', "$partition", 'Mount'))){
    printf ("$0: error: Could not obtain the mountpoint for partition %d\n", $partition);
    next;
  }
  # Mountpoint found, so append skel root dir to it:
  my $skelmountpoint = $mountpoint.$skel_path;

  # Check that the newskel is on that partition
  my @skel_list = $vsdconf->getval ('Partition', "$partition", 'Skel');
  my $skelflag = 0; # Flag to set if skel match has been found
  foreach (@skel_list){
    my $skeltofind = $_;
    if ($skeltofind eq $newskel){
      push (@linkedvs, $vserver);
      $skelflag++;
    }
  }

  # Check that skelflag has been incremented (skel is on same partition)
  if ($skelflag == 0){
    printf ("$0: error: cannot link %s as the\n", $vserver);
    printf ("               skel %s is not valid\n", $newskel);
    next;
  }

  # Now, check that new skel physically exists before attempting to unlink any of the vs:
  die "$0: error: directory containing the new skel not found" unless -d "$skelmountpoint/$newskel";
  
  # check that the virtual server is not already linked to the new skel 
  if ( $newskel eq $currentskel ){ 
     printf( "$0: error: %s already linked to %s\n ", $vserver, $newskel );
     next;
   } 

  # Stop the vs from running
  printf ("Stopping virtual server: $vserver\n");
  system ("$sbindir/vsboot", "--stop", "$vserver");

  if (! defined ($pid[$pidc] = fork ())){
    die "could not fork: $!";
  } 
  elsif ($pid[$pidc] == 0) {
    # Child code:
    # Create list of files to unlink

    
    chdir "$skelmountpoint/$currentskel" or die "unable to change to $skelmountpoint/$currentskel: $!";
    printf ("Unlinking %s from %s\n", $vserver, $currentskel);
    # Create list of dirs to create, exclude certain dirs:
    foreach $file (glob ("*")){
      my (@unlink_files) = ();
      next if $file =~ /etc/;
      next if $file =~ /home/;
      #find the softlinks from the old skel
      find_slinks_to_unlink ("$file",  "$skelmountpoint/$currentskel", "$mountpoint/vs/$vserver", \@unlink_files);
      # Find the files that are from the old skel
      find_files_to_unlink ("$file", "$skelmountpoint/$currentskel", "$mountpoint/vs/$vserver", \@unlink_files);
      # Unlink those files
      unlinkvsfile ("$mountpoint/vs/$vserver", \@unlink_files);
    }
    # Create list of files to link to
    chdir "$skelmountpoint/$newskel" or die "$0: error: unable to change to $skelmountpoint/$newskel: $!";
    printf ("Linking %s to %s\n", $vserver, $newskel);
    foreach $file (glob ("*")){
      next if (($file =~ /dev/) || ($file =~ /proc/) || ($file =~ /tmp/) || ($file =~ /var/));
      my (@link_files) = ();
      # find any new directories that need to be created
      # Make sure top level dir is included
      if ( -d $file) {
        push (@new_dirs, $file)
      }
      
      find_slinked_dirs ("$file", "$skelmountpoint/$newskel","$mountpoint/vs/$vserver", \@new_slinked_dirs);
      find_dirs ("$file", "$skelmountpoint/$newskel","$mountpoint/vs/$vserver", \@new_dirs);
      # Make them
      mklinkdirs ("$skelmountpoint/$newskel","$mountpoint/vs/$vserver", \@new_dirs);
      mklinkdirs ("$skelmountpoint/$newskel","$mountpoint/vs/$vserver", \@new_slinked_dirs);
      # Find all files in the new skel and create a copy in the virtual server
      find_files_to_link ("$file", "$skelmountpoint/$newskel", "$mountpoint/vs/$vserver", \@link_files);
      if (($file =~ /etc/) || ($file =~ /home/) || ($file =~ /root/)){
        copyvsfile ("$skelmountpoint/$newskel", "$mountpoint/vs/$vserver", \@link_files);
      }
      else {
        linkvsfile ("$skelmountpoint/$newskel", "$mountpoint/vs/$vserver", \@link_files);
      }
    }

    # compare the two skels & remove any dirs
    chdir "$mountpoint/vs/$vserver" 
                     or die "$0: error: unable to change to $mountpoint/vs/$vserver: $!";
    printf ("Cleaning directories on virtual server: %s\n", $vserver);
    find_dirs (".", "$mountpoint/vs/$vserver", "$skelmountpoint/$newskel", \@rm_dirs);
    rmlinkdirs ("$mountpoint/vs/$vserver", \@rm_dirs);

    # Restart vs
    printf ("Starting virtual server: %s\n", $vserver);
    system ("$sbindir/vsboot", "--start", "$vserver");

    # if it got this far then it was linked
    # use vsdadm to change the skel the vs is linked to.
    system ("$sbindir/vsdadm", "part_setskelvs", "$hstserver", "$vserver", "$newskel");
    exit 0;
  } 
  else {
    # Parent code:
    # Let a max of 10 children spin off, then reap, and then release next ten
    if ($pidc++ == 10){
      for ($i = 0; $i < $pidc; $i++){
        waitpid ($pid[$i], 0);
        if ($? != 0){
          $failcount++;
        }
      }
    }
  }
}

# All vs should now be linking, wait for the stragglers
for ($i = 0; $i < $pidc; $i++){
  waitpid ($pid[$i], 0);
  if ($? != 0){
    $failcount++;
  }
}

if ($failcount > 0){
  printf ("$0: error: %d virtual server[s] failed to link propperly. \n", $failcount);
  printf ("$0: Please check the output for errors\n");
}

# Change vsd.conf so that skel entry reflects change
# Addition of mod_part makes this section redundant.
#foreach (@linkedvs){
#  my $vserver = $_;
#  $vsdconf->setval ('VirtualServer', "$vserver", "Skel", "$newskel");
#}

# Job done
 
######################################################################
# Subroutine to link the virtual server. To be included into a module
# at some point
######################################################################
sub linkvsfile 
{
  my ($fromdir) = $_[0];
  my ($todir) = $_[1];
  my ($file_array_ref) = $_[2];
  my ($file);

  foreach $file (@$file_array_ref){
#    printf ("Linking %s to %s\n", "$fromdir/$file", "$todir/$file");
    if ( ! -f "$todir/$file" && ! -l "$todir/$file" ){
      link ("$fromdir/$file", "$todir/$file") or die "failed to link $todir/$file: $!";
    }
  }
}

sub copyvsfile
{
  my ($fromdir) = $_[0];
  my ($todir) = $_[1];
  my ($file_array_ref) = $_[2];
  my ($file);

  foreach $file (@$file_array_ref){
#    printf ("Copying %s to %s\n", "$fromdir/$file", "$todir/$file");
    if ((-f "$fromdir/$file" && -f "$todir/$file")  && compare ("$fromdir/$file", "$todir/$file") == 1){ 
      system ("cp", "$fromdir/$file", "$todir/$file.linkupdate");
    }
    else {
      system ("cp", "$fromdir/$file", "$todir/$file");
    }
  }
}

# Subroutine to unlink a virtual server. To be included into a module
sub unlinkvsfile 
{
  my ($dir) = $_[0];
  my ($file_array_ref) = $_[1];
  my ($file);

  foreach $file (@$file_array_ref){
#    printf ("Unlinking %s \n", "$dir/$file");
    unlink ("$dir/$file") or die "failed to unlink $dir/$file: $!";
  }
}

sub mklinkdirs
{
  my ($skeldir) = $_[0];
  my ($dir) = $_[1];
  my ($dir_array_ref) = $_[2];
  my ($newdir);

  foreach $newdir (@$dir_array_ref){
    if ( ! -d "$dir/$newdir" && ! -l "$dir/$newdir" ){
      if ( -l "$skeldir/$newdir" ){
         my $slink = readlink( "$skeldir/$newdir" );
         symlink( "$slink", "$dir/$newdir" ) or warn "unable to create symbolic link : $dir/$newdir : $!";
      }
      else { 	
         mkdir ("$dir/$newdir", 0755) or warn "unable to create directory $dir/$newdir: $!";
      }
    }
  }
}

sub rmlinkdirs
{
  my ($dir) = $_[0];
  my ($dir_array_ref) = $_[1];
  my ($newdir, $i);

  for ($i = @$dir_array_ref; $i >= 0; $i--){
    $newdir = @$dir_array_ref[$i];
    next if (($newdir =~ /proc/) || ($newdir =~ /var/) || ($newdir =~ /home/)
             || ($newdir =~ /dev/) || ($newdir =~ /tmp/) || ($newdir =~ /root/));
    if ( -d "$dir/$newdir"){
      rmdir ("$dir/$newdir"); # or die "unable to remove dir $dir/$newdir: $!"; 
      # Don't care if it fails, if dir wasn't empty then we didn't want it removed
    }
  }
}

sub find_dirs
{
    my $dir = $_[0];
    my $fromdir = $_[1];
    my $todir = $_[2];
    my $dir_array_ref = $_[3];
    my $file; # Var to contain the filename in the foreach loop
    my $dirtocheck = new DirHandle $dir;

    if (defined $dirtocheck){
      while (defined ($file = $dirtocheck->read)){
        next if $file =~ /^\.\.?$/;
	next if ( -l "$fromdir/$dir/$file");
        if ( -d "$fromdir/$dir/$file" && ! -d "$todir/$dir/$file"){
                   push (@$dir_array_ref, "$dir/$file"); 
        }
        &find_dirs ("$dir/$file", "$fromdir", "$todir", $dir_array_ref) if (-d "$fromdir/$dir/$file" && ! -l "$fromdir/$dir/$file");
      } # end while
      undef $dirtocheck;
    }
}    


sub find_slinked_dirs
{
    my $dir = $_[0];
    my $fromdir = $_[1];
    my $todir = $_[2];
    my $dir_array_ref = $_[3];
    my $file; # Var to contain the filename in the foreach loop
    my $dirtocheck = new DirHandle $dir;
    if (defined $dirtocheck){
      while (defined ($file = $dirtocheck->read)){
#	printf(" dir being checked : %s/%s \t", $dir,$file);
        next if $file =~ /^\.\.?$/;
        if ( ( -d "$fromdir/$dir/$file" && ! -d "$todir/$dir/$file") && ( -l "$fromdir/$dir/$file" && ! -l "$todir/$dir/$file") ){
                   push (@$dir_array_ref, "$dir/$file"); 
        }
        &find_slinked_dirs ("$dir/$file", "$fromdir", "$todir", $dir_array_ref) if (-d "$fromdir/$dir/$file" && ! -l "$fromdir/$dir/$file");
      } # end while
      undef $dirtocheck;
    }
}    

sub find_files_to_link
{
    # Needs to be executed in the dir in which the comparison starts
    # so that the relative path can be used for recursion.
    my $dir = $_[0];
    my $fromdir = $_[1];
    my $todir = $_[2];
    my $files_array_ref = $_[3];
    my $file; # Var to contain the filename in the foreach loop
    my $dirtocheck = new DirHandle $dir;

    if (defined $dirtocheck){
      while (defined ($file = $dirtocheck->read)){
        next if $file =~ /^\.\.?$/;
        if ( -f "$dir/$file"){
          push (@$files_array_ref, "$dir/$file"); 
        }
        &find_files_to_link ("$dir/$file", "$fromdir", "$todir", $files_array_ref) 
                                          if (-d "$dir/$file" && ! -l "$dir/$file");
      } # end while
      undef $dirtocheck;
    }
}    

sub find_files_to_unlink
{
    # Needs to be executed in the dir in which the comparison starts
    # so that the relative path can be used for recursion.
    my $dir = $_[0];
    my $fromdir = $_[1];
    my $todir = $_[2];
    my $files_array_ref = $_[3];
    my $file; # Var to contain the filename in the foreach loop
    my $dirtocheck = new DirHandle $dir;

    if (defined $dirtocheck){
      while (defined ($file = $dirtocheck->read)){
        next if $file =~ /^\.\.?$/;
        &find_files_to_unlink ("$dir/$file", "$fromdir", "$todir", $files_array_ref)
                                          if (-d "$dir/$file" && ! -l "$dir/$file");
        if ( ! -l "$fromdir/dir/file" && ! -l "$todir/$dir/$file"){
	  if ( -f "$fromdir/$dir/$file" && -f "$todir/$dir/$file"){
            if (compare ("$fromdir/$dir/$file", "$todir/$dir/$file") == 0){
              push (@$files_array_ref, "$dir/$file");
	    }
          }
        }
     } # end while
      undef $dirtocheck;
    }
}    


sub find_slinks_to_unlink
{
    # Needs to be executed in the dir in which the comparison starts
    # so that the relative path can be used for recursion.
    my $dir = $_[0];
    my $fromdir = $_[1];
    my $todir = $_[2];
    my $files_array_ref = $_[3];
    my $file; # Var to contain the filename in the foreach loop
    my $dirtocheck = new DirHandle $dir;

    if (defined $dirtocheck){
      while (defined ($file = $dirtocheck->read)){
        next if $file =~ /^\.\.?$/;
        &find_slinks_to_unlink ("$dir/$file", "$fromdir", "$todir", $files_array_ref)
                                          if (-d "$dir/$file" && ! -l "$dir/$file");
        if ( -l "$fromdir/$dir/$file" && -l "$todir/$dir/$file"){
            push (@$files_array_ref, "$dir/$file");
	  }
     } # end while
      undef $dirtocheck;
    }
}    

sub find_files_to_del
{
    # Needs to be executed in the dir in which the comparison starts
    # so that the relative path can be used for recursion.
    my $dir = $_[0];
    my $fromdir = $_[1];
    my $todir = $_[2];
    my $files_array_ref = $_[3];
    my $file; # Var to contain the filename in the foreach loop
    my $dirtocheck = new DirHandle $dir;

    if (defined $dirtocheck){
      while (defined ($file = $dirtocheck->read)){
        next if $file =~ /^\.\.?$/;
        &find_files_to_unlink ("$dir/$file", "$fromdir", "$todir", $files_array_ref)
                                          if (-d "$dir/$file" && ! -l "$dir/$file");
        if ( -f "$fromdir/$dir/$file" && ! -f "$todir/$dir/$file"){
          push (@$files_array_ref, "$dir/$file"); 
        }
      } # end while
      undef $dirtocheck;
    }
}    

sub usage
{
  printf ("$0: usage: [--virtualserver, -v vsone[,vstwo]] [--skel, -s skelname] \n");
  exit -1;
}
