# -*-perl-*-

# $Id: Grub.plm,v 1.10 2008/04/01 21:58:33 thomas Exp $

# What package to handle grub.conf

# $Log: Grub.plm,v $
# Revision 1.10  2008/04/01 21:58:33  thomas
# Allow minsize to be specified in the what config
#
# Revision 1.9  2007/08/31 14:55:01  thomas
# smaller min size for amd64_cent5
#
# Revision 1.8  2007/07/05 18:22:34  thomas
# allow kernel options that take no value
#
# Revision 1.7  2006/09/12 21:15:55  thomas
# enable use_ide_scsi="no" for CentOS
#
# Revision 1.6  2004/07/06 14:45:01  thomas
# documentation
#
# Revision 1.5  2004/07/02 19:01:10  thomas
# Allow xml to specify what other kernels to configure (smp/bigmem/hugemem/...)
#
# Revision 1.4  2004/03/05 15:11:39  thomas
# doc updates
# cconf->what
#
# Revision 1.3  2004/01/13 16:48:31  thomas
# catch-up 1/13/04
#
# Revision 1.2  2003/12/15 16:28:35  thomas
# check-in for 0.1.1
#
# Revision 1.1.1.1  2003/12/02 14:47:35  thomas
# Initial version before export to 0.1.1
#

=head1 NAME

What::Node::Grub - manage /boot/grub/grub.conf

=head1 DESCRIPTION

This B<What> configuration package controls the content of the file
B</boot/grub/grub.conf>.

Attributes of the base node B</Grub> control the general parameters of
B<default>, B<timeout>, B<splashimage>, B<password>, B<serial> and
B<terminal>.  In this context, default provides not a number but the
B<name> or a grub node.

Certain children of the B<Grub> root node have special meaning:

=over 4

B</Grub/append> -- Attributes of this node will be added to each
B<kernel> line in the configuration as I<attribute>=I<value> pairs.
If the I<value> is zero length, the equals sign I<=> will be ommitted.

B</Grub/generic> -- child nodes of this node should be named for
kernel versions, with attributes matching the form given below for
regular grub stanza nodes.  Nodes included in the generic section will
be modified to provide 6 stanzas, for non-SMP, SMP, and bigmem
kernels, each further modified for multiuser and single-user boots.
The single user boots will all be locked, requiring a password to be
executed.

=back

Any other B<Grub> nodes B</Grub/I<name>> create stanzas in the grub.conf file.
I<name> is a unique key that describes the kernel.  Most of the other
attributes are self-descriptive.  The B<default> attribute provides
the I<name> of the default kernel; the configuration module will
convert that to an appropriate index when it creates the B<grub.conf>
file.

Kernel parameters of the form hdB<n>=ide-scsi will be copied from the
current B<grub.conf> file.  Also, the grub B<root> line and kernel
B<root=> option will be copied from the current grub.conf file.

=head1 EXAMPLES

The following grub.conf and B<What> xml are equivalent, assuming a system with an existing grub.conf with /dev/hda1 as the boot device:

 ********
 XML Version
 ********
 <node path='/Grub'
   default='2.4.20-19.7'
   timeout='0'
   splashimage='(hd0,0)/boot/grub/splash.xpm.gz'
   password='--md5 $1$wBYp//$M8sEDe1PI7jklPllL4CEV/' />

 <node path='/Grub/2.4.20-19.7'
   title='Red Hat Linux (2.4.20-19.7)'
   initrd='/boot/initrd-2.4.20-19.7.img'
   kernel='/boot/vmlinuz-2.4.20-19.7 ro apm=off' />


 ********
 Final grub.conf
 ********
 default=0
 timeout=10
 splashimage=(hd0,0)/boot/grub/splash.xpm.gz
 password --md5 $1$wBYp//$M8sEDe1PI7jklPllL4CEV/
 title Red Hat Linux (2.4.20-19.7)
        root (hd0,0)
        kernel /boot/vmlinuz-2.4.20-19.7 ro root=/dev/hda1 apm=off
        initrd /boot/initrd-2.4.20-19.7.img


Create single- and multi-user kernel for non-smp, smp, and hugemem
kernels.  Lock the single-user kernels to prevent tampering.

 <node path="/Grub/generic/2.4.21-15.0.2.TL"
       title="Tao Linux (2.4.21-15.0.2.TL)"
       initrd="/boot/initrd-2.4.21-15.0.2.TL.img"
       kernel="/boot/vmlinuz-2.4.21-15.0.2.TL ro apm=off"
       variants="smp,hugemem" />



=cut

package What::Node::Grub;

use strict;
use FileHandle;
use Data::Dumper;

use base qw(What::Node);

our %opts = ();

# Set the priority for this module in $::
$::module_priority{'Grub'} = 20;

###############################################################################
# Apply the current configuration

sub apply {
  my $class = shift;
  my $section = shift;
  my $what = shift;
  my $sname = shift;
  my $cname = shift;

  my ($children, $child, $newgrub, $attrs, $cattrs, $attr, $id, $st, $fh);
  my ($oldgrub, $newname, $dir, $base, $default, $defaultname, $minsize);
  my (@stanzas, $rootdev, $kernel_options, $variant, $append_attrs);
  my $oldname =
    (defined ($::opts{root}) ? $::opts{root} : '') . '/boot/grub/grub.conf';

  # Expand any generic kernel arguments
  if (defined ($children = $class->getchildren ($section, path => '/generic'))) {
    foreach $child (keys %$children) {
      my $attrs = $children->{$child}->getattrs ($section);
      $class->debug (3, "Creating Grub nodes from generic node $child...\n");
      # Base
      $class->setattrs (
			$section,
			{
			 %$attrs,
			},
			$child,
			1
		       );
      # Base single user
      $cattrs = {%$attrs};
      $cattrs->{title} =~ s/$/ Single User/;
      $cattrs->{kernel} .= ' single';
      $class->setattrs (
			$section,
			{
			 %$cattrs,
			 lock => 1,
			},
			"${child}_single",
			1
		       );

      # Now do the variants
      if (defined $attrs->{variants}) {
	foreach $variant (split /\s*,\s*/, $attrs->{variants}) {
	  # Multi-user
	  $cattrs = {%$attrs};
	  $cattrs->{title} =~ s/$/ $variant/;
	  $cattrs->{initrd} =~ s/\.img/${variant}\.img/;
	  $cattrs->{kernel} =~ s/^(\S+)/$1${variant}/;
	  $class->setattrs (
			    $section,
			    {
			     %$cattrs,
			    },
			    "${child}${variant}",
			    1
			   );

	  # Single user
	  $cattrs = {%$attrs};
	  $cattrs->{title} =~ s/$/ ${variant} Single User/;
	  $cattrs->{initrd} =~ s/\.img/${variant}\.img/;
	  $cattrs->{kernel} =~ s/^(\S+)/$1${variant}/;
	  $cattrs->{kernel} .= ' single';
	  $class->setattrs (
			    $section,
			    {
			     %$cattrs,
			     lock => 1,
			    },
			    "${child}${variant}_single",
			    1
			   );
	}
      }
    }
  }

  $attrs = $class->getattrs ($section);

  ($rootdev, $kernel_options) = $class->readcurrent ($oldname, $attrs->{use_ide_scsi});

  # Check for extra arguments to append to the kernel lines
  $append_attrs = $class->getattrs ($section, path => 'append');
  if (defined $append_attrs) {
    foreach $attr (sort keys %$append_attrs) {
      $kernel_options .= (length $append_attrs->{$attr}) ? " $attr=$append_attrs->{$attr}" : " $attr";
    }
  }


  my $failed = 0;
  foreach $attr ('default', 'timeout', 'password') {
    if (!exists ($attrs->{$attr}) || !defined ($attrs->{$attr})) {
      $class->err ("Missing required Grub attribute $attr!\n");
      $failed = 1;
    }
  }
  $failed and return 0;

  # Write out the root section
  $newgrub = <<__Eof__
###
# This grub.conf was generated automatically by What::Node::Grub
# Hand-made changes will be lost!!
###

__Eof__
  ;
  $defaultname = $attrs->{default};
  $attrs->{default}='0';
  foreach $attr ('default', 'timeout', 'splashimage') {
    if (defined ($attrs->{$attr})) {
      $newgrub .= "$attr=$attrs->{$attr}\n";
    }
  }
  foreach $attr ('serial', 'terminal', 'password') {
    defined $attrs->{$attr} or next;
    $newgrub .= "$attr $attrs->{$attr}\n";
  }

  # Now add kernel stanzas
  $children = $class->getchildren;
  if (!defined ($children->{$defaultname})) {
    $class->err ("No such default Grub stanza '$defaultname'!\n");
    return 0;
  }

  $newgrub .= $class->getstanza ($section, $defaultname, $rootdev, $kernel_options);
  foreach $child (sort keys %$children) {
    $child eq $defaultname and next;
    $child eq 'generic' and next;
    $child eq 'append' and next;
    $newgrub .= $class->getstanza ($section, $child, $rootdev, $kernel_options);
  }

  # Install the new content
  $minsize = defined ($attrs->{minsize}) ? $attrs->{minsize} : 800;
  $class->check_and_replace_content ($oldname, $newgrub, $minsize, 1);
  return 1;
}

###############################################################################

sub getstanza {
  my $class = shift;
  my $section = shift;
  my $name = shift;
  my $rootdev = shift;
  my $kernel_options = shift;

  my $attrs = $class->getattrs ($section, path => $name);
  defined ($attrs) or return '';

  defined ($attrs->{title}) or return '';
  my $ret = "title $attrs->{title}\n";
  ($class->checkbool ($attrs->{lock})) and $ret .= "    lock\n";
  defined ($rootdev) and $ret .= "    root $rootdev\n";
  $attrs->{kernel} and $ret .= "    kernel $attrs->{kernel} $kernel_options\n";
  $attrs->{initrd} and $ret .= "    initrd $attrs->{initrd}\n";
  return $ret;
}

###############################################################################
# Return a two element array that consists of the root option and 
# additions to the kernel line

sub readcurrent {
  my $class = shift;
  my $oldfile = shift;
  my $use_ide_scsi = shift;

  my $it = {};
  my ($rootdev, $kernelroot) = (undef, '');
  my $fh = new FileHandle $oldfile, 'r';
  if (defined $fh) {
    while (<$fh>) {
      # read /proc/ide instead of the current grub file
      # (/^\s*kernel\s.*(hd\S=ide-scsi)/) and $it->{$1} = 1;
      (/^\s*kernel\s.*(root=\S+)/) and $kernelroot = $1;
      (/^\s*root\s+(\S+)/) and $rootdev = $1;
    }
    undef $fh;
  }

  # Find all the cdrom devices and make them hd?=ide-scsi
  # Backward compatibility: use_ide_scsi="0" to disable the check
  if (!defined ($use_ide_scsi) || $class->checkbool ($use_ide_scsi)) {
    my ($dh, $de, $media);
    $dh = new DirHandle "/proc/ide";
    while (defined ($de = $dh->read)) {
      $de =~ /hd[a-z]/ or next;
      $fh = new FileHandle "/proc/ide/$de/media", 'r';
      defined ($fh) or next;
      chomp ($media = $fh->getline);
      undef $fh;
      $media =~ /^cdrom$/ and $it->{"$de=ide-scsi"} = 1;
    }
  }

  return ($rootdev, join (' ', keys %$it, $kernelroot));

}


###############################################################################

1;
