#
# Copyright 2001-2003 Sun Microsystems, Inc.  All rights reserved.
# Use is subject to license terms.
#
# ident	"@(#)Install.pm	1.50	03/09/29 SMI"
#
# Install class

package Cluster::Install;
use strict;
use CGI;
use Cluster::Common;
use Cluster::RunCommand;
use Socket;
use Sun::Solaris::Utils qw(gettext);
use vars qw(@ISA $VERSION);
$VERSION = '1.00';
@ISA = qw(Cluster::Common);

my ($ifconf, $public) = (1, 2); # Flags
my $PFEXEC = "/usr/bin/pfexec";

# Global constants
use constant TRUE          => 0;
use constant FALSE         => 1;

# Solaris OS versions
my $SOLARIS_VERSION_8       = "5.8";
my $SOLARIS_VERSION_9       = "5.9";

# Sun Cluster subdirs based on Solaris versions
my $SC_SOLARIS_8_SUBDIR     = "Sol_8";
my $SC_SOLARIS_9_SUBDIR     = "Sol_9";

##############################################################################
#
# Class Methods
#
##############################################################################

sub new {
    my $proto = shift;
    my $class = ref($proto) || $proto;
    my $self  = {};
    bless ($self, $class);
    return $self;
}

# Return the primary interface which is hosting the physical hostname
sub get_primary_interface {
    my ($self) = @_;
    my (@interfaces, @hostname, $phys);

    # Get the hostname
    my $hostname = $self->get_hostname();
    
    # Open the /etc directory to get the list of entries
    opendir(DIR, "/etc");
    my @allfiles = readdir(DIR);
    closedir(DIR);
    
    # Read through the entries and find the ones that start with 'hostname.'
    my @list;
    foreach my $file (@allfiles) {
	push @list, $file if ($file =~ /hostname./);
    }
    
    # Go though them and find the one with the hostname
    my $primary_adapter;
    foreach my $file (@list) {
	chomp $file;
	open (FILE, "/etc/$file");
	my $line = <FILE>;
	chomp $line;
	close(FILE);
	
	if ($line eq $hostname) {
	    my @fileparts;
	    @fileparts = split /\./, $file;
	    $primary_adapter = $fileparts[1];
	    last;
	}
    }

    return $primary_adapter;
}

# Return all interfaces other than the phys- adapter
sub get_interfaces {
    my ($self) = @_;
    my (@interfaces, @hostname, $phys);

    # Get the hostname
    my $hostname = $self->get_hostname();
    
    # Open the /etc directory to get the list of entries
    opendir(DIR, "/etc");
    my @allfiles = readdir(DIR);
    closedir(DIR);
    
    # Read through the entries and find the ones that start with 'hostname.'
    my @list;
    foreach my $file (@allfiles) {
	push @list, $file if ($file =~ /hostname./);
    }
    
    # Go though them and find the one with the hostname
    my $primary_adapter;
    foreach my $file (@list) {
	chomp $file;
	open (FILE, "/etc/$file");
	my $line = <FILE>;
	chomp $line;
	close(FILE);
	
	if ($line eq $hostname) {
	    my @fileparts;
	    @fileparts = split /\./, $file;
	    $primary_adapter = $fileparts[1];
	    last;
	}
    }

    @interfaces = `$PFEXEC /usr/bin/awk '{print \$3 \$2}' /etc/path_to_inst | tr -d \\" | egrep -e 'hme|qfe|ge|eri|ce|bge|e1000g|iprb'`;

    my @interconnects;
    for (my $i=0; $i<=$#interfaces; $i++) {
	chomp $interfaces[$i];

	push @interconnects, $interfaces[$i]
	    unless ($interfaces[$i] eq $primary_adapter);
    }

    # Get the wildcat interfaces
    my $wrsmconf = "/bin/wrsmconf";
    my (@wrsmconf_output, @adapters);

    if (-f $wrsmconf) {
	    @wrsmconf_output = `$PFEXEC $wrsmconf topology | grep $hostname`;
    }
 
    foreach my $line (@wrsmconf_output) {
	    chomp $line;
	    my @parts = split /\s+/, $line;
	    push @interconnects, "wrsm" . $parts[2];
    }

    $self->syslog("Found interfaces: @interconnects");
    return sort @interconnects;
}

sub get_remote_primary_interface {
    my ($self, $nodename) = @_;
    my (@interfaces, $response, $location, $node, @nodelist);
    
    $location = "/cgi-bin/installation/node_interfaces.pl";
    ($response) = $self->get_http($nodename, $self->get_http_port(), $location);
    
    @interfaces = split / /, $response;
    return @interfaces;
}

sub get_remote_interfaces {
    my ($self, $nodename) = @_;
    my (@interfaces, $response, $location, $node, @nodelist);
    
    $location = "/cgi-bin/installation/node_interfaces.pl";
    ($response) = $self->get_http($nodename, $self->get_http_port(), $location);
    
    @interfaces = split / /, $response;
    return @interfaces;
}

sub verify_cdrom_path {
    return 0;
}

sub member {
    my ($self, $item, @array) = @_;

    # Return position of element if we find it
    for (my $i = 0; $i <= $#array; $i++) {
        if ($item eq $array[$i]) {
            return $i;
        }
    }

    # return -1 if we didn't find element
    return -1;
}

sub call_remote_scinstall {
    my ($self, $nodename, $nodenumber, $q) = @_;
    my $response;

    my $cluster_name = $q->param('cluster_name');
    my $cluster_size = $q->param('cluster_size');
    my $cdrom_path = $q->param('cdrom_path');
    my $ds_cdrom_path = $q->param("ds_cdrom_path");
    my $transport1 = $q->param("node_$nodenumber.transport_1");
    my $transport2 = $q->param("node_$nodenumber.transport_2");
    my $firstnode = $q->param("node_1.name");
    my $install_nfs = $q->param("install_nfs");

    my $location = "/cgi-bin/installation/scinstall.pl?cluster_name=$cluster_name&cluster_size=$cluster_size&cdrom_path=$cdrom_path&ds_cdrom_path=$ds_cdrom_path&install_nfs=$install_nfs&transport1=$transport1&transport2=$transport2&nodename=$nodename&nodenumber=$nodenumber&firstnode=$firstnode";

    ($response) = $self->get_http($nodename, $self->get_http_port(), $location);
    return $response;
}


# Get the nodelist during installation mode
sub get_nodelist {
    my ($self, $q) = @_;
    my (@nodelist);
  
    if (defined $q->param('cluster_size')) {
	for (my $i=1; $i<=$q->param('cluster_size'); $i++) {
	    if (defined $q->param("node_$i.name")) {
		push @nodelist, $q->param("node_$i.name");
	    }	
	}
    }
    
    return @nodelist;
}

sub check_server_install {
    my ($self, $host) = @_;
    my ($response);

    my $location = "/cgi-bin/node/ping.pl";
    ($response) = $self->get_http($host, $self->get_http_port(), $location,
	undef, 1);
    chomp $response;

    if ($response ne 'OK') {
	return 1;
    }
    
    return 0;
}

sub check_auth {
    my ($self, $host) = @_;
    my ($response);

    my $location = "/cgi-bin/node/ping_auth.pl";
    ($response) = $self->get_http($host, $self->get_http_port(), $location,
	undef, 1);
    chomp $response;

    if ($response ne 'OK') {
	return 1;
    }

    return 0;
}

sub appendfile_local {
    my ($self, $filename, $string) = @_;

    # Open the file. Error out if we didn't get a subprocess pid from
    # the open. The tee may fail to access the file, but there's no
    # way of finding out until the close() below.
    my $ret = open(FILE, "| /usr/bin/pfexec /usr/bin/tee -a $filename " .
		   "> /dev/null");
    if ($ret == 0) {
	    $self->syslog("open() of $filename failed");
	    close FILE;
	    return 1;
    }

    # Write to the file. We can't check for success here, since
    # printing to the pipe will always succeed and return 1.
    print FILE "$string\n";

    # Close the file and check for success. If the above open()
    # failed, we'll find out here.
    $ret = close FILE;
    if (!defined $ret || $ret eq "") {
	    $self->syslog("close() of $filename failed");
	    return 1;
    }
}

sub appendfile_remote {
    my ($self, $host, $filename, $string) = @_;

    # Turn spaces into plusses
    $string =~ s/ /+/g;

    # Turn # into %23
    $string =~ s/#/%23/g;

    # Set the script to be accessed
    my $location = "/cgi-bin/installation/sds/appendfile_local.pl";
    $location .= "?filename=$filename&string=$string";

    # Request
    my @output = $self->get_http($host, $self->get_http_port(), $location,
	undef, 1);

    # Syslog errors
    foreach my $line (@output) {
	$self->syslog($line);
    }
}

sub do_format_remote {
    my ($self, $host, $ptype, $cname) = @_;

    # Set the script to be accessed
    my $location = "/cgi-bin/installation/sds/do_format_local.pl";
    $location .= "?ptype=$ptype&cname=$cname";
    
    # Request
    my @output = $self->get_http($host, $self->get_http_port(), $location,
	undef, 1);

    # Syslog errors
    foreach my $line (@output) {
	$self->syslog($line);
    }
}

sub get_metadb_slice {
    my ($self, $node) = @_;
    my $location= "/cgi-bin/installation/sds/get_metadb_slice.pl";

    my ($output) = $self->get_http($node, $self->get_http_port(), $location);
    chomp($output);
    $self->syslog("$node returned $output as the metadb slice.");
    return $output;
}

sub verify_sds {

    my ($self, $node) = @_;
    my $location = "/cgi-bin/installation/verify_sds.pl?node=$node";

    my @errors = $self->get_http($node, $self->get_http_port(), $location);
    $self->syslog("$node returned preinstall-check-sds errors: @errors");
    return @errors;

} # verify_sds

sub get_install_state {
    my ($self, $node, $step) = @_;
    my $location = "/cgi-bin/installation/install_state.pl?step=$step";
    my ($state) = $self->get_http($node, $self->get_http_port(), $location);
    chomp $state;
    return $state;
}


sub verify_logical_addresses {
    my ($self, $node, @addr_list) = @_;
    my $location = "/cgi-bin/installation/verify_addresses.pl?";

    foreach my $address (@addr_list) {
	$location .= "address=$address&";
    }

    my @errors = $self->get_http($node, $self->get_http_port(), $location);
    $self->syslog("$node returned address errors: @errors");
    return @errors;
}

sub check_patch_errors {
    my ($self, $node, $error_file) = @_;
    my $location = "/cgi-bin/installation/check_patch_errors.pl?error_file=".
        "$error_file";
    my (@errors) = $self->get_http($node, $self->get_http_port(), $location);
    return @errors;
}

sub copy_uncompress_patches {
    my ($self, $node, $patch_path, $uncompressed_patch_path, $error_file, $q) = @_;

    my $location = "/cgi-bin/installation/copy_uncompress_patches.pl?".
	"patch_path=$patch_path&".
	"uncompressed_patch_path=$uncompressed_patch_path&".
	"error_file=$error_file&".
	"node=$node";
    $self->get_http($node, $self->get_http_port(), $location);
    return;
}

sub get_patch_list {
    my ($self, $node, $uncompressed_patch_path, $q) = @_;
    
    my $location = "/cgi-bin/installation/get_patch_list.pl?".
	"node=$node&".
	"uncompressed_patch_path=$uncompressed_patch_path";
    my @patch_list = $self->get_http($node, $self->get_http_port(), $location);
    return @patch_list;
}

sub remove_done_patches_state_file {
    my ($self, $node) = @_;

    my $location = "/cgi-bin/installation/remove_done_patches_state_file.pl";
    $self->get_http($node, $self->get_http_port(), $location);
}

sub verify_paths {
    my ($self, $node, $q) = @_;
    
    my $install_sds = $q->param("install_sds");
    my $install_nfs = $q->param("install_nfs");
    my $install_apache = $q->param("install_apache");

    my $cdrom_path = $q->param("cdrom_path");
    my $ds_cdrom_path = $q->param("ds_cdrom_path");
    my $patch_path = $q->param("patch_path");

    my $location = "/cgi-bin/installation/verify_paths.pl";
    $location .= "?cdrom_path=$cdrom_path";

    # Only validate the optional patch path if it's been specified
    if (defined $patch_path && $patch_path ne '') {
	$location .= "&patch_path=$patch_path";
    }

    if ($install_sds eq "yes") {

	    # Only check the Solaris CDROM path for Solaris 8
	    if ($self->is_solaris8() == TRUE) {
		    my $solaris_cdrom_path = $q->param("solaris_cdrom_path");
		    $location .= "&solaris_cdrom_path=$solaris_cdrom_path";
	    }		    
		
	    if ($install_nfs eq "yes" || $install_apache eq "yes") {
		    $location .= "&ds_cdrom_path=$ds_cdrom_path";
	    }
    }

    my @errors = $self->get_http($node, $self->get_http_port(), $location);
    $self->syslog("$node returned path errors: @errors");
    return @errors;
}

# Find scinstall, using the given Sun Cluster CDROM path and the .cdtoc file
sub get_scinstall_path($$) {
	my ($self, $cdrom_path) = @_;
	my $cdtoc_path = "$cdrom_path/.cdtoc";

	if (-T $cdtoc_path) {
		my ($prodname, $prodvers) = ();
		open(CDTOC, $cdtoc_path);
		while (my $line = <CDTOC>) {
			if ($line =~ /^PRODNAME/) {
				chomp $line;
				my @parts = split /=/, $line;
				$prodname = $parts[1];
			} elsif ($line =~ /^PRODVERS/) {
				chomp $line;
				my @parts = split /=/, $line;
				$prodvers = $parts[1];			
			}
		}
		close(CDTOC);

		if ($prodname && $prodvers) {
			# Look up the os version and construct the proper path
			my $path = "$cdrom_path/$prodname" . "_" . "$prodvers/";

			# Check for Solaris 8
			if ($self->is_solaris8() == TRUE) {
				$path .= $SC_SOLARIS_8_SUBDIR;
			# Check for Solaris 9
			} elsif ($self->is_solaris9() == TRUE) {
				$path .= $SC_SOLARIS_9_SUBDIR;
			# Unknown OS
			} else {
				return undef;
			}

			$path .= "/Tools/scinstall";
			return $path;
		}
	}

	return undef;
}

# Run autodiscovery on the nodes and transports in $q
sub run_autodiscovery {
    my ($self, $q) = @_;

    for (my $i = 1; $i <= $q->param("cluster_size"); $i++) {
	my $location = "/cgi-bin/installation/get_autodiscover.pl?start=1";

	# Don't check wildcat interfaces
	if ($q->param("node_$i.transport_1") !~ /wrsm/) {
		$location .= "&if1=".$q->param("node_$i.transport_1");
	}

	if ($q->param("node_$i.transport_2") !~ /wrsm/) {
		$location .= "&if2=".$q->param("node_$i.transport_2");
	}

	my @info = $self->get_http($q->param("node_$i.name"),
	    $self->get_http_port(), $location);
    }
    sleep 5;
    for (my $i = 1; $i <= $q->param("cluster_size"); $i++) {
	$self->parse_autodiscovery($q->param("node_$i.name"));
    }
}

# Parse /var/cluster/spm/autodiscover
# Fill in hash tables:
# {'nodes'} with list of nodes
# {'if'} with node:interfaces
# {'matrix'}{$sendif}{$recvif} with 1, 2, or 3 if we received from that combo
# {'public'}{"node:if"} is "" if private, otherwise an error message

sub parse_autodiscovery {
    my ($self, $recvnode) = @_;

    # Set a flag so we don't fetch from the same node multiple times
    if (defined $self->{'fetched'}{$recvnode}) {
	return 1;
    }

    # Get autodiscovery info from the node
    my $location = "/cgi-bin/installation/get_autodiscover.pl?fetch=1";
    my @info = $self->get_http($recvnode, $self->get_http_port(), $location);

    if ($#info < 0) {
	return 0;
    }

    $self->{'fetched'}{$recvnode} = 1;

    $self->{'nodes'}{$recvnode} = 1;
    while ($#info >= 0) {
	$_ = shift @info;
	if (/^([^:]*):([^:]*):([^:]*)/) {
	    my ($recvif, $sendnode, $sendif) = ($1, $2, $3);
	    $self->{'nodes'}{$sendnode} = 1;
	    $recvif = "$recvnode:$recvif";
	    $sendif = "$sendnode:$sendif";
	    $self->{'if'}{$recvif} = 1;
	    $self->{'if'}{$sendif} = 1;
	    $self->{'matrix'}{$sendif}{$recvif} |= 1;
	    $self->{'matrix'}{$recvif}{$sendif} |= 2;
	} elsif (/quit/) {
	    my ($if);
	    foreach $if (keys %{$self->{'if'}}) {
		$self->{'error'} =
		    gettext("Node access problem encountered: IPMP conflict?");
	    }
	} else {
	    my ($if, $flag) = split(/ /);
	    $self->{'if'}{"$recvnode:$if"} = 1;
	    if ($flag & $ifconf) {
		$self->{'public'}{"$recvnode:$if"} =
		    gettext("Interface ifconfig'd up");
	    } elsif ($flag & $public) {
		$self->{'public'}{"$recvnode:$if"} =
		    gettext("Traffic detected on interface");
	    } else {
		$self->{'public'}{"$recvnode:$if"} = "";
	    }
	}
    }
    return 1;
}

# Do a udp broadcast request to see what nodes are out there waiting
# for installs.
# Sets cgi parameters node_x.name where x=1..cluster_size
# Returns cluster_size
sub determine_nodes {
    my ($self, $q) = @_;

    my ($addr) = "255.255.255.255"; # Broadcast

    my ($port) = $self->get_http_port();
    my ($msg);

    socket(S, PF_INET, SOCK_DGRAM, getprotobyname('udp')) || die("socket $!");
    my ($sin);
    $sin = sockaddr_in($port, inet_aton($addr));
    send(S, "Query", 0, $sin) || die("send $!");
    my ($rin) = "";
    vec($rin, fileno(S), 1) = 1;
    my ($rout);
    my (%nodes);
    my ($buf);
    while (1) {
	my ($num) = select($rout=$rin, undef, undef, 5);
	if ($num <= 0) {
	    last;
	}
	recv(S, $buf, 1000, 0) || die("recv $!");
	if ($buf ne "") {
	    $nodes{$buf} = 1;
	}
    }

    # Set the parameters with the node list
    my ($num) = 0;
    foreach my $node (sort keys %nodes) {
	$num++;
	$q->param("node_".$num.".name", $node);
    }
    return $num;
}

# Determine potential transports.  This logic is fairly minimal.
# Look for an interface that exists on all nodes and appears to be private.
sub determine_transports {
    my ($self, $q, @nodes) = @_;
    my ($i);
    my ($hostname) = `hostname`;
    chop $hostname;
    my ($num) = 1;

    for ($i = 0; $i <= $#nodes; $i++) {
	$self->parse_autodiscovery($nodes[$i]);
    }

    IFACE: foreach $i (sort keys %{$self->{'if'}}) {
	my ($node, $if) = split(/:/, $i);
	# See if the interface is on node 0
	if ($node eq $nodes[0]) {
	    print "<!-- Testing $if -->\n";
	    my ($j);
	    # See if the corresponding interface exists on the other nodes
	    for ($j = 0; $j <= $#nodes; $j++) {
		if (!defined($self->{'if'}{$nodes[$j].":".$if})) {
		    print "<!-- Rejected $if: not on node $nodes[$j] -->\n";
		    next IFACE;
		}
		if ($self->{'public'}{$nodes[$j].":".$if} ne "") {
		    print "<!-- Rejected $if: ".
			$self->{'public'}{$nodes[$j].":".$if}." -->\n";
		    next IFACE;
		}
	    }
	    if ($j > $#nodes) {
		print "<!-- Setting $if as transport $num -->\n";
		for ($j = 1; $j <= $#nodes+1; $j++) {
		    $q->param("node_$j.transport_$num", $if);
		}
		$num++;
		if ($num > 2) {
		    last;
		}
	    }
	}
    }
}

# Validate the transport configuration against autodiscovery data
# Return ref to array of errors and/or warnings
sub validate_transport {
    my ($self, $q) = @_;
    my (@errors, @warnings);
    my (@nodes);
    for (my $i=1; $i<=$q->param('cluster_size'); $i++) {
	my $nodename = $q->param("node_$i.name");
	push @nodes, $nodename;
	my $transport_1 = $q->param("node_$i.transport_1");
	my $transport_2 = $q->param("node_$i.transport_2");
	
	if ($transport_1 eq $transport_2) {
	    push @errors, $q->sprintfn(
		gettext("%1 was selected for both transports on %2. The " .
		"same adapter cannot be used as both transports on a node."),
		$transport_1, $nodename);
	}
    }

    # Can return now if any catastrophic errors
    if ($#errors >= 0) {
	return (\@errors, \@warnings);
    }

    # Validate the information against any autodiscovery information
    $self->run_autodiscovery($q);

    if (defined $self->{'error'}) {
	push @warnings, $self->{'error'};
    }


    CHECK: for (my $i=1; $i<=$q->param('cluster_size'); $i++) {
	for (my $tnum = 1; $tnum <= 2; $tnum++) {

	    my $nodename = $q->param("node_$i.name");
	    if (! defined($self->{'nodes'}{$nodename})) {
		push @warnings, sprintf(
		    gettext("Autodiscovery could not validate node %s"),
		    $nodename);
		next CHECK;
	    }
	    
	    # Get the transport name
	    my $transport = $q->param("node_$i.transport_$tnum");

	    # Skip checking wildcat transports
	    next if ($transport =~ /wrsm/);

	    # Check that interfaces are not public
	    if ($self->{'public'}{"$nodename:$transport"}) {
		push @warnings, $q->sprintfn(
		    gettext("Transport %1 may be public: %2"),
		    "$nodename:$transport",
		    $self->{'public'}{"$nodename:$transport"});
	    } elsif ($self->{'public'}{"$nodename:$transport"} ne "") {
		push @warnings, $q->sprintfn(
		    gettext("Transport %1 may be public: %2"),
		    "$nodename:$transport",
		    $self->{'public'}{"$nodename:$transport"});
	    }

	    # Check that nodes are connected
	    for (my $j=$i+1; $j<=$q->param('cluster_size'); $j++) {
		my $nodename2 = $q->param("node_$j.name");
		my $transport2 = $q->param("node_$j.transport_$tnum");
		if ($self->{'matrix'}{"$nodename:$transport"}
			{"$nodename2:$transport2"} == 0) {
		    push @warnings, $q->sprintfn(
			gettext("%1 may not be communicating with %2."),
			"$nodename:$transport", "$nodename2:$transport2");
		    next CHECK;
		}
	    }
	}
    }
    return (\@errors, \@warnings);
}

#
# Parse the /var/cluster/spm/metaset_db file and create the appropriate
# entries in the /etc/vfstab file on all nodes.
sub append_vfstab_entry() {
	my ($self, $nodename) = @_;

	my $location = "/cgi-bin/installation/append_vfstab_entry.pl";
	$self->get_http($nodename, $self->get_http_port(), $location);
}

#
# Start a button table
#

sub start_button_table() {
	my ($self, $q) = @_;	

	# Print the title table
	print $q->start_table({ BORDER      => 0,
				   CELLSPACING => 0,
				   CELLPADDING => 0,
				   WIDTH => "97%",
				   ALIGN => "center",
				   CLASS       => "action-table"
				   });
	print $q->start_Tr({ HEIGHT => 1});
	print $q->start_td({ COLSPAN => 2,
				CLASS => "action-line-row"});
	print $q->img({ SRC => "/images/dot.gif",
			ALT => "",
			HEIGHT => 1});
	print $q->end_td()."\n";
	print $q->end_Tr()."\n";

	print $q->start_Tr({ HEIGHT => 6});
	print $q->start_td({ COLSPAN => 2});
	print $q->img({ SRC => "/images/dot.gif",
			ALT => "",
			HEIGHT => 6});
	print $q->end_td(), $q->end_Tr(), "\n";

	print $q->start_Tr();
	print $q->start_td({ ALIGN => "right",
				WIDTH => "80%"});
}

# End the install buttons.  If $cancel is defined, a cancel button will
# be displayed.
sub end_button_table {
    my ($self, $q, $cancel) = @_;
    print $q->end_td();
    print $q->start_td({ ALIGN => "right",
			    WIDTH => "20%"});
    if (defined $cancel) {
	print $q->end_form();

	print $q->start_form({ action => '/cgi-bin/installation/cancel.pl',
		method => 'get'
		});
	print $q->submit({NAME => "cancel",
			VALUE => gettext("  Cancel  ")});
    } else {
	print "&nbsp;\n";
    }
    print $q->end_td(), $q->end_Tr(), $q->end_table();
}

# Create a message table with an icon
sub start_message_table {
    my ($self, $q, $img, $alt, $header, $msg) = @_;


    print $q->start_table({	BORDER => 0,
			    CELLSPACING => 0,
			    CELLPADDING => 5,
			    ALIGN => "center",
			    WIDTH => "97%",
			    CLASS => "message-table",
			    });

    print $q->start_Tr();
    print $q->start_td({	NOWRAP => undef,
			    ALIGN => "center",
			    VALIGN => "top",
			    WIDTH => 32,
			    HEIGHT => 32,
			    });
    print $q->img({		SRC => $img,
			    WIDTH => 32,
			    HEIGHT => 32,
			    ALT => $alt,
			    });
    print $q->end_td(),"\n";

    print $q->start_td({	WIDTH => "95%"});
    print $q->p({		CLASS => "message-header-text"}, $header);
    print $q->p({		CLASS => "message-description-text"}, $msg);
}

sub end_message_table {
    my ($self, $q) = @_;
    print $q->end_td(), $q->end_Tr(), $q->end_table(), "\n";
}

# Remove a file on this node
sub remove_local_file {
	my ($self, $file) = @_;
	`$PFEXEC /usr/bin/rm -f $file`;
	return $?;
}

# Call /cgi-bin/installation/sds/get_ctd_disks.ksh on the specified node
sub get_ctd_disks_node {
	my ($self, $node) = @_;
	my (@output, $ctd);	
	$ctd = "/cgi-bin/installation/sds/get_ctd_disks.pl";
	$self->syslog("Running get_ctd_disks.ksh on $node");
	@output = $self->get_http($node, $self->get_http_port(), $ctd);
	return @output;
}

# Call get_ctd_disks_node() serially on all nodes, redirecting the
# output to a file corresponding to each node
sub get_ctd_disks {
	my ($self, $q) = @_;
	my $logdir = $self->get_logdir();
	foreach my $node ($self->get_nodelist($q)) {
		my @output = $self->get_ctd_disks_node($node);
		open OUTPUT, ">$logdir/ctd.$node.$$";
		foreach my $line (@output) {
			print OUTPUT "$line\n";
			$self->syslog($line);
		}
		close OUTPUT;
	}
}

# Call cgi-bin/installation/sds/config_sds on one node. For each node,
# three arguments will be given to config_sds: <node datafile metadb_slice>
#
# node : The node name
#
# datafile : The files generated when get_ctd_disks.pl was previously
#            run on the node.
#
# metadb_slice : This node's dedicated rootdisk partition set aside
#	         for metadb creation.  Placing the /sds filesystem 
#   	         is how the partition is dedicated for the metadb 
#		 replica.  The metadb_slice format is cNtNdNsN 
#		 (e.g. c0t0d0s4).
#
# config_sds will write the rc scripts to the appropriate nodes. If
# any metadb_slice cannot be determined, then the config_sds script
# will not be run and SDS installation will be skipped entirely.
sub config_sds {
	my ($self, $q) = @_;
	my $logdir = $self->get_logdir();
	my @errors = ();

	# Remove appendfile.dat from previous runs
	my $appendfile = "$logdir/appendfile.dat";
	$self->remove_local_file($appendfile);

	# Check for the /sds partition
	my $config = "/opt/SUNWscvw/cgi-bin/installation/sds/config_sds";
	foreach my $node ($self->get_nodelist($q)) {
		my $metadb_slice = $self->get_metadb_slice($node);
		if (defined $metadb_slice) {
			$self->syslog("Successfully located the /sds " .
				      "partition ($metadb_slice) on the " .
				      "rootdisk of node $node.");
			$config .= " $node $logdir/ctd.$node.$$ $metadb_slice";
		} else {
			push (@errors, $q->sprintfn(gettext("Could not locate " .
				    "the /sds partition on the rootdisk " .
				    "of node %1."), $node));

			$self->syslog("Could not locate the /sds partition " .
				      "on the rootdisk of node $node that " .
				      "is required for Solstice DiskSuite " .
				      "installation.");
		}
	}

	# Check to see if errors were encountered when locating the
	# /sds partition before running config_sds
	if ( $#errors == -1 ) {
		# Run the config_sds binary
		$self->syslog("Running: $config");
		my @output = `$PFEXEC $config`;
		if ($? != 0) {
			push (@errors, gettext("&nbsp;&nbsp;Unexpected " .
					       "problems occurred - " .
					       "Skipping Solaris Volume " .
					       "Manager configuration..."));
			
			# Report errors from the execution
			foreach my $line (@output) {
				$self->syslog($line);
			}
		}

		# Check to see if a metaset was configured
		my $rc = new Cluster::RunCommand();
		my @grep = $rc->grep("mirror-1", $appendfile);
		if ($#grep == -1) {
			push (@errors, gettext("Failed to find enough " .
					       "properly configured shared " .
					       "disks from which to create " .
					       "a mirrored diskset."));

			$self->syslog("Failed to find enough properly " .
				      "configured shared disks from which " .
				      "to create a mirrored diskset.");
		}
	}

	return @errors;
}

# 5. Open the appendfile.dat file to create the datafiles that contain 
# the information needed for metadb/metaset/metadevice creation on each
# node. The appendfile.dat file was created by config_sds. By this point,
# we know that appendfile.dat exists, or else we wouldn't be trying to
# install SDS. This is checked for in ds_step1.pl.
sub process_appendfile_dat ($$) {
	my ($self, $q) = @_;
	my $foundfile = 0;
	my $logdir = $self->get_logdir();
	foreach my $node ($self->get_nodelist($q)) {
		my @output = $self->readfile_node($node, "$logdir/appendfile.dat");
		if ($#output > -1) {
			$foundfile++;
			foreach my $line (@output) {
				chomp $line;
				my ($host, $filename, @string, $space);
				($host, $filename, @string) = split / /, $line;
				my $fileline = join ' ', @string;
				$self->appendfile_remote($host, $filename, $fileline);
			}
		}
	}

	if ($foundfile == 0) {
		$self->syslog("Failed to find appendfile.dat on any node!");
		return 1;
	}

	return 0;
}

# Get the contents of appendfile.dat on a specified node
sub readfile_node($$$) {
	my ($self, $node, $file) = @_;
	my $readfile = "/cgi-bin/installation/readfile_local.pl?file=$file";
	$self->syslog("Reading file $file on $node");
	my @output = $self->get_http($node, $self->get_http_port(),
				     $readfile);
	return @output;
}
