#!/usr/bin/perl -w

#       liloconfig -  creating a new lilo.conf file
#       
#       Copyright 2011-2013 Joachim Wiedorn <ad_debian@joonet.de>
#       
#       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.
#       
#       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.

#---- some modules
use strict;
use warnings;
use Getopt::Std;
use Pod::Usage;
use File::Copy;


#---- global variables
my $prog = $0;
$prog =~ s#.*/##;
my $version = "0.2";

#---- parameter check
# h: help, v: verbose, f: force
our $opt_h = 0;
our $opt_v = 0;
our $opt_f = 0;
getopts('hvf');
# define perldoc usage
pod2usage(1) if $opt_h;

#---- other variables
our $liloconf  = "/etc/lilo.conf";
our $conftmp_1 = "/tmp/lilotmp1";
our $conftmp_2 = "/tmp/lilotmp2";
our $liloconfold = $liloconf . ".old";
our $liloconfnew = $liloconf . ".new";
our $fstabconf = "/etc/fstab";

our $idpath = "/dev/disk/by-id";
our $uuidpath = "/dev/disk/by-uuid";
our $lblpath = "/dev/disk/by-label";
our $template = "/usr/share/doc/lilo/examples/lilo.example.conf.gz";

our $rootpart;    # found root part
our $root_dev;    # /dev/hdX9, /dev/sdX9, /dev/md/*
our $root_id;     # UUID, LABEL, ID
our $boot_dev;    # /dev/hdX, /dev/sdX, /dev/md
our $boot_id;     # DISK-ID

#-------------------- main program --------------------

sub main {

	my $exit = 0;

	if (@ARGV == 1) {
		$liloconf = "$ARGV[0]";
		$liloconfold = $liloconf . ".old";
		$liloconfnew = $liloconf . ".new";
	}
	if (-f $liloconf and not $opt_f) {
		print "$prog: $liloconf already exist! Please use '-f' for overwriting.\n";
		$exit = 1;
	}
	else {
		$exit = create_lilo_conf()
	}		
	return $exit;
}

#-------------------- subroutines --------------------

sub create_lilo_conf {
	
	my $found = 0;
	my $exit = 1;

	# search for root device in fstab and convert it
	$found = detect_root_device();

	# convert root device to boot device
	if ($found) { $found = convert_boot_device(); }

	# finally write new lilo.conf file
	if ($found) { $exit = write_lilo_conf(); }

	return $exit;
}

sub detect_root_device {
	
	# read fstab and find root device; 
	my $found = read_fstab();

	# identify root device: root_dev and root_id
	if ($found) { $found = convert_root_device(); }
	
	return $found;
}

sub read_fstab {
	
	my $root_part;
	my $mountpoint;
	my $broken_fstab = 1;
	my $base_fstab = 0;
	my $found = 1;

	# check fstab for root device
	if (-f $fstabconf) {
		# Parsing fstab for the root partition
		open(FSTAB, "<$fstabconf") or die "$prog: couldn't open $fstabconf: $!\n";

		while (<FSTAB>) {
			# Search magic string which indicates a base filesystem
			$base_fstab = 1 if /^# UNCONFIGURED FSTAB FOR BASE SYSTEM/;
			next if /^#/;	  # ignore comment lines

			s/^[ \t]+//;	  # remove space or tab at begin of the line
			($root_part,$mountpoint) = split(/[ \t]+/);
			next unless defined $mountpoint;    # ignore empty lines too

			# stop if we found the root device...
			if ($mountpoint eq '/') {
				$broken_fstab = 0;
				last;
			}
		}
		close(FSTAB) or die "$prog: couldn't close $fstabconf: $!\n";
	}

	if ($base_fstab) {
		print "E: It seems you want configure the base filesystem \n" .
		      "and I'm therefore simply going to exit successfully \n" .
		      "without trying to actually configure LILO properly. \n";
		$found = 0;
	}
	if ($broken_fstab) {
		print "E: It seems the file /etc/fstab is not properly \n" .
		      "configured: no root partition '/' found! \n";
		$found = 0;
	}
	# save the found root device
	$rootpart = $root_part;

	return $found;
}

sub convert_root_device {
	
	my $found = 1;
	my $root_disk = '';
	my $root_link;
	# global variables: $root_dev, $root_id

	if ($rootpart =~ /\/dev\//) {
		$root_disk = $rootpart;

		if (-b $root_disk) {
			$root_dev = $root_disk;
			if($opt_v) { print "Convert root option $root_disk into UUID\n"; }
			$root_id = find_id_link($root_disk,$uuidpath);

			if (not -l "$uuidpath/$root_id") {
				if($opt_v) { print "W: could not find UUID for $root_disk!\n"; }
				## than we want use root_dev in lilo.conf
				#$found = 0;
			}
			else {
				# finally add uuid label
				$root_id = "UUID=" . $root_id;
			}
		}
		else {
			if($opt_v) { print "E: cannot check $root_disk: device does not exist!\n"; }
			$found = 0;
		}
	}
	elsif ($rootpart =~ /^UUID/ or $rootpart =~ /^LABEL/) {
		$root_link = $rootpart;
		$root_link =~ s{\"}{}g;
		$root_link =~ s{^LABEL=}{/dev/disk/by-label/};
		$root_link =~ s{^UUID=}{/dev/disk/by-uuid/};

		if (-l $root_link) {
			$root_id = $rootpart;
			$root_disk = readlink($root_link);
			$root_disk =~ s{\.\./\.\./}{/dev/};

			if (-b $root_disk) { $root_dev = $root_disk; }
			else {
				if($opt_v) { print "E: cannot check $root_link: link does not exist!\n"; }
				$found = 0;
			}
		}
		else {
			print "E: cannot check $root_link: link does not exist!\n";
			$found = 0;
		}
	}
	else {
		print "E: cannot use uncommon $rootpart found as root device!\n";
		$found = 0;
	}

	return $found;
}

sub find_id_link {
	
	my $olddev = $_[0];
	my $path_id = $_[1];
	my @sellinks;
	my $_idlink;
	my $_actlink;
	my $newdevid = '';

	opendir(MYDH, "$path_id") or die("cannot open $path_id: $! \n");
	@sellinks = grep(!/\-part\d\d?$/, grep(!/^\.\.?$/, readdir(MYDH)));
	@sellinks = sort(@sellinks);
	closedir(MYDH);

	foreach $_idlink (@sellinks) {
		chomp $_idlink;
		if(not $_idlink =~ /^usb/ and length($_idlink) > 10) {
			$_actlink = readlink("$path_id/$_idlink");
			$_actlink =~ s{\.\./\.\./}{/dev/};
			if($opt_v) { print "** try: $_actlink => $_idlink \n"; }
			
			# stop if we find the right link...
			if($_actlink eq $olddev) {
				$newdevid = $_idlink;
				if($opt_v) { print "** convert: $_actlink => $path_id/$_idlink \n\n"; }
				last;
			}
		}
	}

	if(not $newdevid) {
		if($opt_v) { print "W: $olddev not converted: link not useful\n\n"; }
	}
	
	return ($newdevid);
}

sub convert_boot_device {

	my $found = 1;
	my $boot_disk = '';
	my $boot_link;
	# global variables: $boot_dev, $boot_id

	if (-b $root_dev) {
		if ($root_dev =~ /\/dev\/md/) {
			# search if the found partition is a raid volume
			$boot_disk = check_raid($root_dev);
		}
		else {
			# find the right block device name
			$boot_disk = $root_dev;
			$boot_disk =~ s/\d+$//;
		}

		if (-b $boot_disk) {
			# set global variable boot_dev
			$boot_dev = $boot_disk;
		}
		else { 
			print "E: boot device $boot_disk does not exist! \n";
			$found = 0;
		}
	}
	else {
		print "E: could not find root device $root_dev! \n";
		$found = 0;
	}

	if ($found) {
		if($opt_v) { print "Convert boot option $boot_disk into DISK ID\n"; }
		$boot_id = $idpath . "/" . find_id_link($boot_disk,$idpath);

		if(not -l "$boot_id") {
			if($opt_v) { print "W: could not find DISK ID for $boot_disk!\n"; }
			## not so important, than using boot_dev in lilo.conf
			#$found = 0;
		}
	}

	return $found;
}

sub check_raid {
	
	my $part = $_[0];
	my $mdname;
	my $md;
	my @devices;

	# check if the found partition is a raid volume
	if($part =~ /\/dev\/md/)
	{
		$mdname = $part;
		$mdname =~ s/\/dev\///;
		$mdname =~ s/\///;
		$md = `grep $mdname /proc/mdstat`;
	
		@devices = split(" ", $md);
		@devices = sort(@devices[4..$#devices]);
		$part = "/dev/" . $devices[0];
		$part =~ s/\[.*$//;

	}
	return $part;
}

sub write_lilo_conf {
	
	my @status;
	my $exit = copy_template();

	if (not $exit) {
		# create lilo.conf.new
		write_boot_option();
		write_image_config();
	}

	if (-f $liloconf and not -f $liloconfold) {
		# move old lilo.conf to lilo.conf.old
		@status = stat($liloconf);
		move ($liloconf, $liloconfold) or die "Cannot rename file: $!\n";
		utime ($status[9],$status[9],$liloconfold);
		chmod (0600,$liloconfold);
		print "Old file moved to: $liloconfold \n";
	}
	if (-f $liloconfnew) {
		move ($liloconfnew, $liloconf) or die "Cannot move file: $!\n";
		chmod (0600,$liloconf);
		print "New file created as: $liloconf \n";
		print "Now you must execute '/sbin/lilo' to " . 
		      "activate this new configuation!\n\n";
	}
	else {
		print "E: Cannot find temporary file $conftmp_1!\n";
		$exit = 1;
	}
	
	return $exit;
}

sub copy_template {

	my $endreached = 0;
	my $exit = 0;
	
	# copy template config
	if (-f $template) {
		system("gzip -d -c $template >$conftmp_1") if ($template =~ /\.gz$/);
		system("cat $template >$conftmp_1") if ($template =~ /\.conf$/);

		open(CONFTMP1, "<$conftmp_1") or die "$prog: couldn't open $conftmp_1: $!\n";
		open(CONFTMP2, ">$conftmp_2") or die "$prog: couldn't open $conftmp_2: $!\n";

		while (<CONFTMP1>) {
			if (/first\ example/) {
				$endreached = 1;
			}
			unless ($endreached) {
				print CONFTMP2 $_;
			}
		}
		close(CONFTMP1) or die "$prog: couldn't close $conftmp_1: $!\n";;
		close(CONFTMP2) or die "$prog: couldn't close $conftmp_2: $!\n";;
	}
	else {
		open(CONFTMP2, ">$conftmp_2") or die "$prog: couldn't open $conftmp_2: $!\n";
		print CONFTMP2 "# /etc/lilo.conf

### LILO global section ###

#large-memory
lba32
boot = /dev/sda
map = /boot/map
install = menu
menu-scheme = Wb:Yr:Wb:Wb
prompt
timeout = 100
vga = normal
#default = Linux

### LILO per-image section ###

"; 
		close(CONFTMP2) or die "$prog: couldn't close $conftmp_2: $!\n";;
	}
	
	return $exit;
}

sub write_boot_option {
	
	my $oldline = '';
	my $newline = '';
	my $ok = 0;

	open(MYFH_NEW, "> $liloconfnew") or die "Cannot open file: $!";
	open(MYFH_TMP, "< $conftmp_2") or die "Cannot read file: $!";

	while (<MYFH_TMP>) {
		# line read from MYFH_TMP
		$oldline = $_;

		if (/^boot/ and $ok == 0) {
			if ($boot_id) {
				$oldline = "#boot = $boot_dev\n";
				$newline = "boot = $boot_id\n";
				print MYFH_NEW $oldline;
				if($opt_v) { print $oldline; }
			}
			else {
				$oldline = "boot = $boot_dev\n";
			}
			print MYFH_NEW $newline;
			if($opt_v) { print $newline; }
			if($opt_v) { print "\n"; }

			# convert only one time
			$ok = 1;
		}
		else {
			print MYFH_NEW $oldline;
		}
	}
	close(MYFH_TMP);
	close(MYFH_NEW);
}

sub write_image_config {

	my $image;
	my $initrd;
	my $nr;
	my $nr2;

	# search for kernel image files
	my @vmlinuz = readpipe("/bin/ls -t -1 /boot/vmlinuz-* 2>/dev/null");

	# append to new lilo.conf
	open(MYFH_NEW, ">> $liloconfnew") or die "Cannot open file: $!";

	# create some line for each kernel image
	$nr = 0;
	foreach $image (@vmlinuz) {
		# search for kernel initrd file
		chomp $image;
		$initrd = $image;
		$initrd =~ s/vmlinuz/initrd\.img/;
		$nr2 = $nr + 1;

		print MYFH_NEW     'image = ' . $image . "\n";
		if($opt_v) { print 'image = ' . $image . "\n"; }

		if ($nr == 0) {
			print MYFH_NEW     "\t"  . 'label = "Linux"' . "\n";
			if($opt_v) { print "\t"  . 'label = "Linux"' . "\n"; }
		}
		elsif ($nr == 1) {
			print MYFH_NEW     "\t"  . 'label = "Linux Old"' . "\n";
			if($opt_v) { print "\t"  . 'label = "Linux Old"' . "\n"; }
		}

		if ($root_id) {
			print MYFH_NEW     "\t"  . '#root = ' . $root_dev . "\n";
			if($opt_v) { print "\t"  . '#root = ' . $root_dev . "\n"; }
			print MYFH_NEW     "\t"  . 'root = "' . $root_id . '"' . "\n";
			if($opt_v) { print "\t"  . 'root = "' . $root_id . '"' . "\n"; }
		}
		else {
			print MYFH_NEW     "\t"  . 'root = ' . $root_dev . "\n";
			if($opt_v) { print "\t"  . 'root = ' . $root_dev . "\n"; }
		}

		print MYFH_NEW     "\t"  . 'read-only' . "\n";
		if($opt_v) { print "\t"  . 'read-only' . "\n"; }
		print MYFH_NEW     "#\t" . 'restricted' . "\n";
		if($opt_v) { print "#\t" . 'restricted' . "\n"; }
		print MYFH_NEW     "#\t" . 'alias = ' . "$nr2" . "\n";
		if($opt_v) { print "#\t" . 'alias = ' . "$nr2" . "\n"; }
		print MYFH_NEW     "#\t" . 'optional' . "\n";
		if($opt_v) { print "#\t" . 'optional' . "\n"; }

		if (-f $initrd) {
			print MYFH_NEW     "\t"  . 'initrd = ' . $initrd . "\n";
			if($opt_v) { print "\t"  . 'initrd = ' . $initrd . "\n"; }
		}
		else {
			if($opt_v) { print "initrd $initrd could not be found!\n" }
		}

		print MYFH_NEW     "\n";
		if($opt_v) { print "\n"; }

		$nr++;
		last if ($nr > 1);
	}

	close(MYFH_NEW);
}		
		
	
main();

__END__


=head1 NAME

liloconfig - create new lilo.conf file (with diskid and uuid)

=head1 SYNOPSIS

liloconfig [-h] [-v] [-f] [lilo.conf]

=head1 DESCRIPTION

liloconfig is an simple program for creating a new lilo.conf file.
After creating the new configuration file you must execute '/sbin/lilo'.

liloconfig use the lilo.example.conf file as template. In the final
lilo.conf file you find many useful comments for custom changes.

=head1 EXAMPLES

Lines in the configuration file /etc/lilo.conf:

  ### LILO global section ###

  #large-memory
  lba32
  boot = /dev/sda
  map = /boot/map
  install = menu
  menu-scheme = Wb:Yr:Wb:Wb
  prompt
  timeout = 100
  vga = normal
  #default = Linux

  ### LILO per-image section ###

  #boot = /dev/sda
  boot = /dev/disk/by-id/ata-SAMSUNG_SV1604N_S01FJ10X999999

  image = /boot/vmlinuz-3.5.0-trunk-686
      label = "Linux"
      #root = /dev/sda1
      root = "UUID=18843936-00f9-4df0-a373-000d05a5dd44"
      read-only
  #   restricted
  #   alias = 1
  #   optional
      initrd = /boot/initrd.img-3.5.0-trunk-686

  image = /boot/vmlinuz-3.2.0-4-686
      label = "Linux Old"
      #root = /dev/sda1
      root = "UUID=18843936-00f9-4df0-a373-000d05a5dd44"
      read-only
  #   restricted
  #   alias = 2
  #   optional
      initrd = /boot/initrd.img-3.2.0-4-686

=head1 OPTIONS

=over 4

=item B<-h>

Print a brief help.

=item B<-v>

Print verbose messages.

=item B<-f>

Force overriding existing lilo.conf.

=back

=head1 AUTHOR

B<liloconfig> was written by Joachim Wiedorn.

=cut
