#!/usr/bin/perl -w

#*********************************************************************
# 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 2 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.
#
# A copy of the GNU General Public License is available as
# `/usr/share/common-licences/GPL' in the Debian GNU/Linux distribution
# or on the World Wide Web at http://www.gnu.org/copyleft/gpl.html. You
# can also obtain it by writing to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
#*********************************************************************

use strict;

# treat all warnings about uninitialised values as errors
use warnings FATAL => qw(uninitialized);

################################################################################
#
# @file setup-storage
#
# @brief The main function of setup-storage - the tool to configure the
# partitioning from within FAI.
#
# This is an implementation from scratch to properly support LVM and RAID. The
# input format is documented in @ref parser.pm
#
# Some (developer) documentation may be found on
# https://wiki.fai-project.org/index.php/Setup-storage
#
# Some coding conventions:
# - no whitespace after ( and before )
# - keyword<whitespace>(...)
# - do not start a new line for {
# - } goes on a line on its own
# - one blank line after sub ...
#
# @author Christian Kern, Michael Tautschnig, Thomas Lange
#
################################################################################

package FAI;

my $version = "3.0";
$|=1;

# command line parameter handling
use Getopt::Std;
$main::VERSION = $version;
$Getopt::Std::STANDARD_HELP_VERSION = 1;
our ($opt_X, $opt_f, $opt_h, $opt_d, $opt_s, $opt_D, $opt_L,$opt_y,$opt_p); # the variables for getopt
(&getopts('Xf:hdsD:L:yp') && !$opt_h) || die <<EOF;
setup-storage version $version

USAGE: [-X]                     no test, your harddisks will be formated
                                default: only test, no real formating
       [-f<config-filename>]    default: parse classes
       [-d]                     enable debug output (equivalent to debug=1)
       [-s]                     perform syntax check only and exit
       [-D<disks>]              override disklist variable by space-separated <disks>
       [-L<logdir>]             use <logdir> instead of LOGDIR variable
       [-h]                     print this help message
       [-p]                     print the commands that would be executed, then exit.
       [-y]                     print disk variables as YAML file into disk_var.yml
EOF

# include all subparts, which are part of the FAI perl package
use lib "/usr/share/fai/setup-storage/";
use Init;
use Volumes;
use Parser;
use Sizes;
use Commands;
use Fstab;
use Exec;

sub prtdebug {

  # for debugging purposes to print the hash structures
  use Data::Dumper;

  print "Current disk layout in \%current_config\n";
  print Dumper \%FAI::current_config;

  print "Current LVM layout in \%current_lvm_config\n";
  print Dumper \%FAI::current_lvm_config;

  print "Current RAID layout in \%current_raid_config\n";
  print Dumper \%FAI::current_raid_config;

  print "Current device tree in \%current_dev_children\n";
  print Dumper \%FAI::current_dev_children;
}

# enable debug mode, if requested using -d
$opt_d and $FAI::debug = 1;

# Really write any changes to disk
$opt_X and $FAI::no_dry_run = 1;
warn "setup-storage is running in test-only mode\n" unless ($FAI::no_dry_run);

# syntactic checks only
$opt_s and $FAI::check_only = 1;

# Find out whether $LOGDIR is usable or default to /tmp/fai
$opt_L and $FAI::DATADIR = $opt_L;
if (! -d $FAI::DATADIR) {
  !defined($opt_L) and defined ($ENV{LOGDIR}) and die
    "Environment variable LOGDIR is set, but $FAI::DATADIR is not a directory\n";
  mkdir $FAI::DATADIR or die
    "Failed to create directory $FAI::DATADIR\n";
  warn "Created data directory $FAI::DATADIR\n";
}

# $disklist may be provided by the environment
my $disklist = $ENV{disklist};
$opt_D and $disklist = $opt_D;
if (! defined($disklist)) {
  &FAI::in_path("/usr/lib/fai/fai-disk-info") or die "/usr/lib/fai/fai-disk-info not found\n";
  $disklist = `/usr/lib/fai/fai-disk-info | sort`;
}

@FAI::disks = split( /[\n ]/, $disklist);
if ($FAI::debug) {
  print "disklist: ";
  print "$_\n" foreach(@FAI::disks);
}
warn "No disks defined in \$disklist\n" unless (@FAI::disks);

# the config source file
my $config_file = undef;
# use the config file, if given
open($config_file, $opt_f) or die "Failed to open config file $opt_f\n" if ($opt_f);
unless ($opt_f) {
  defined ($ENV{classes}) or
    die "Environment variable classes is not set and -f was not given\n";
  # see which class file to use
  foreach my $classfile (reverse split(/\s+/, $ENV{classes})) {
    next unless (-r "$ENV{FAI}/disk_config/$classfile");
    open($config_file, "$ENV{FAI}/disk_config/$classfile")
      or die "Failed to open $ENV{FAI}/disk_config/$classfile\n";
    $opt_f = "$ENV{FAI}/disk_config/$classfile";
    last;
  }
}

# if we could not find any matching class file, bail out
defined ($config_file) or die "No matching disk_config found\n";

# start the parsing - thereby $FAI::configs is filled
warn "Starting setup-storage $version\n";
print "Using config file: $opt_f\n";
&FAI::run_parser($config_file);

# make sure there are no empty disk_config stanzas
&FAI::check_config;

if ($FAI::check_only) {
  print "Syntax ok\n";
  exit 0;
}

# first find the proper way to tell udev to settle
$FAI::udev_settle = "udevadm settle --timeout=10" if (&FAI::in_path("udevadm"));
$FAI::udev_settle = "udevsettle --timeout=10" if (&FAI::in_path("udevsettle"));
defined($FAI::udev_settle) or die "Failed to find determine a proper way to tell udev to settle; is udev installed?";
`$FAI::udev_settle`;

# read the sizes and partition tables of all disks listed in $FAI::disks
&FAI::get_current_disks;

# see whether there are any existing LVMs
$FAI::uses_lvm and &FAI::in_path("pvdisplay") and &FAI::get_current_lvm;

# see whether there are any existing RAID devices
$FAI::uses_raid and &FAI::in_path("mdadm") and &FAI::get_current_raid;

# mark devices as preserve, where not all already done so and check that only
# defined devices are marked preserve
&FAI::propagate_and_check_preserve;

# debugging only: print the current configuration
if ($FAI::debug) {
  use Data::Dumper;
  prtdebug;
}

# compute the new LVM and partition sizes; do the partition sizes first to have
# them available for the the volume group size estimation
&FAI::compute_partition_sizes;
&FAI::compute_lv_sizes;

# print the current contents of $FAI::configs
$FAI::debug and print "Desired disk layout in %FAI::configs\n";
$FAI::debug and print Dumper \%FAI::configs;
$FAI::debug and print "Desired device tree in %FAI::dev_children\n";
$FAI::debug and print Dumper \%FAI::dev_children;

# generate the command script
&FAI::build_disk_commands;
$FAI::uses_raid and &FAI::build_raid_commands;
&FAI::build_btrfs_commands;
$FAI::uses_lvm and &FAI::build_lvm_commands;
&FAI::build_cryptsetup_commands;
&FAI::order_commands;

# run all commands
# debugging only: print the command script
if ($FAI::debug) {
  foreach (&numsort(keys %FAI::commands)) {
    defined($FAI::commands{$_}{cmd}) or &FAI::internal_error("Missing command entry for $_");
    print "$_:" . $FAI::commands{$_}{cmd} . "\n";
    defined($FAI::commands{$_}{pre}) and print "\tpre: " . $FAI::commands{$_}{pre} . "\n";
    defined($FAI::commands{$_}{post}) and print "\tpost: " . $FAI::commands{$_}{post} . "\n";
  }
}

# only print the commands that would be executed
if ($opt_p) {
  print "These commands would be executed:\n";
  foreach (&numsort(keys %FAI::commands)) {
    next if ($FAI::commands{$_}{cmd} eq "true");
    print "$FAI::commands{$_}{cmd}\n";
  }
  exit 0;
}

# run the commands (if $FAI::no_dry_run is set)
foreach (&numsort(keys %FAI::commands)) {
  `$FAI::udev_settle`;
  next if ($FAI::commands{$_}{cmd} eq "true");
  &FAI::execute_command($FAI::commands{$_}{cmd});
}

# clean crypt key files
unless (defined ($ENV{'FAI_KEEP_CRYPTKEYFILE'})) {
  foreach (@FAI::crypttab) {
    my $fname = (split)[0];
    unlink "$FAI::DATADIR/$fname";
  }
}

# generate the proposed fstab contents
# wait for udev to set up all devices
`$FAI::udev_settle`;
my @fstab = &FAI::generate_fstab(\%FAI::configs);

# print fstab
$FAI::debug and print "$_\n" foreach (@fstab);

# write the proposed contents of fstab to $FAI::DATADIR/fstab
if ($FAI::no_dry_run) {
  open(FSTAB, ">$FAI::DATADIR/fstab")
    or die "Failed to open $FAI::DATADIR/fstab for writing\n";
  print FSTAB "$_\n" foreach (@fstab);
  close FSTAB;
}

# write variables to $FAI::DATADIR/disk_var.sh
# debugging
$FAI::debug and print "$_=\${$_:-$FAI::disk_var{$_}}\n"
  foreach (keys %FAI::disk_var);

if ($FAI::no_dry_run) {
  # List all bootable devices. Can be used by later tasks to install a
  # boot loader. Using Variable PHYSICAL_BOOT_DEVICES.
  my @boot_devices;
  foreach (keys %FAI::configs) {
    $_ =~ /PHY_(.+)/;
    if ( $1 && defined $FAI::configs{$_}{'bootable'}
      &&  $FAI::configs{$_}{'bootable'} > 0 )  {
      push @boot_devices, $1;
    }
  }
  @boot_devices = sort @boot_devices;

  open(DISK_VAR, ">$FAI::DATADIR/disk_var.sh")
    or die "Unable to write to file $FAI::DATADIR/disk_var.sh\n";
  print DISK_VAR "$_=\${$_:-$FAI::disk_var{$_}}\n" foreach (keys %FAI::disk_var);
  print DISK_VAR "PHYSICAL_BOOT_DEVICES=\"", join(" ", @boot_devices), "\"\n";
  close DISK_VAR;

  if ($opt_y) {
    open(DISK_YML, ">$FAI::DATADIR/disk_var.yml")
      or die "Unable to write to file $FAI::DATADIR/disk_var.yml\n";

    print DISK_YML "---\n# disk_var.sh as YAML\n";
    foreach (keys %FAI::disk_var) {
      $FAI::disk_var{$_} =~ s/"//g;
      if ( $FAI::disk_var{$_} =~ m/\s/ ) {
	# print an array
	print DISK_YML "$_:\n";
	my $a = join("\n  - ", split(/ /,$FAI::disk_var{$_}));
	print DISK_YML "  - $a\n";
      } else {
	print DISK_YML "$_: $FAI::disk_var{$_}\n";
      }
    }
    print DISK_YML "PHYSICAL_BOOT_DEVICES:\n";
    my $a = join("\n  - ", @boot_devices);
    print DISK_YML "  - $a\n";
    close DISK_YML;
  }
}

# print crypttab
$FAI::debug and print "$_\n" foreach (@FAI::crypttab);

# write the proposed contents of fstab to $FAI::DATADIR/fstab
if ($FAI::no_dry_run && scalar(@FAI::crypttab)) {
  open(CRYPTTAB, ">$FAI::DATADIR/crypttab")
    or die "Failed to open $FAI::DATADIR/crypttab for writing\n";
  print CRYPTTAB "$_\n" foreach (@FAI::crypttab);
  close CRYPTTAB;
}


# at the end print the whole config again
if ($FAI::debug) {
  print "==========================================\n";
  print "=  Variables at the end of setup-storage =\n";
  print "==========================================\n";
  prtdebug;
}
