#!/usr/bin/perl -w
#******************************************************************************
#  $Id$
#******************************************************************************
#  Copyright (C) 2003-2006 The Regents of the University of California.
#  Produced at Lawrence Livermore National Laboratory (cf, DISCLAIMER).
#  Written by Mark Grondona <mgrondona@llnl.gov>
#  UCRL-CODE-230739.
#
#  This file is part of edac-utils.
#
#  This 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 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.
#****************************************************************************/

use strict;
use File::Basename;
use Getopt::Long;

my $prefix      = "/usr";
my $sysconfdir  = "/etc";
my $dmidecode   = find_prog ("dmidecode");
my $modprobe    = find_prog ("modprobe")  or exit (1);

my %conf        = ();
my %bus         = ();
my $prog        = basename $0;
$conf{labeldb}  = "$sysconfdir/edac/labels.db";
$conf{labeldir} = "$sysconfdir/edac/labels.d";
$conf{mbconfig} = "$sysconfdir/edac/mainboard";


my $status      = 0;

my $usage       = <<EOF;
Usage: $prog [OPTIONS...]
 --quiet            Quiet operation.
 --mainboard        Print mainboard vendor and model for this hardware.
 --status           Print status of EDAC drivers.
 --print-labels     Print Motherboard DIMM labels to stdout.
 --register-labels  Load Motherboard DIMM labels into EDAC driver.
 --delay=N          Delay N seconds before writing DIMM labels.
 --labeldb=DB       Load label database from file DB.
 --help             This help message.
EOF

parse_cmdline();

if (  $conf{opt}{mainboard} || $conf{opt}{print_labels} 
   || $conf{opt}{register_labels}) {

    get_mainboard_info();

    if ($conf{opt}{mainboard} eq "report") {
        print "$prog: mainboard: ", 
              "$conf{mainboard}{vendor} $conf{mainboard}{model}\n";
    }

    if ($conf{opt}{print_labels}) { 
        print_dimm_labels ();

    }
    if ($conf{opt}{register_labels}) {
        register_dimm_labels ();
    } 

}

if ($conf{opt}{status}) {
    $status = print_status ();
    exit ($status ? 0 : 1);
}

exit (0);

sub parse_cmdline
{
    $conf{opt}{mainboard} = '';
    $conf{opt}{print_labels} = 0;
    $conf{opt}{register_labels} = 0;
    $conf{opt}{status} = 0;
    $conf{opt}{quiet} = 0;
    $conf{opt}{delay} = 0;

    my $rref = \$conf{opt}{report};
    my $mref = \$conf{opt}{mainboard};

    Getopt::Long::Configure ("bundling");
    my $rc = GetOptions ("mainboard:s" =>     sub { $$mref = $_[1]||"report" },
                         "help" =>            sub {usage (0)},
                         "quiet" =>           \$conf{opt}{quiet},
                         "print-labels" =>    \$conf{opt}{print_labels},
                         "register-labels" => \$conf{opt}{register_labels},
                         "delay:s" =>         \$conf{opt}{delay},
                         "labeldb=s" =>       \$conf{labeldb},
                         "status" =>          \$conf{opt}{status});
        
    usage(1) if !$rc;

    usage (0) if !grep $conf{opt}{$_}, keys %{$conf{opt}}; 

    if ($conf{opt}{delay} && !$conf{opt}{register_labels}) {
        log_error ("Only use --delay with --register-labels\n");
        exit (1);
    }
}

sub usage
{
    my ($rc) = @_;
    print "$usage\n";
    exit ($rc);
}

sub run_cmd
{
    my @args = @_;
    system ("@args");
    return ($?>>8);
}


sub print_status
{
    my $status = 0;
    open (MODULES, "/proc/modules")
         or die "Unable to open /proc/modules: $!\n";

    while (<MODULES>) {
       $status = 1 if /_edac/;
    }

    print "$prog: drivers ", ($status ? "are" : "not"), " loaded.\n"
        unless $conf{opt}{quiet};

    return ($status);
}


sub get_mainboard_info {
    my ($vendor, $model);

    if ($conf{opt}{mainboard} && $conf{opt}{mainboard} ne "report") {
        ($vendor, $model) = split (/[: ]/, $conf{opt}{mainboard}, 2);
    }

    if (!$vendor || !$model) {
        ($vendor, $model) = guess_vendor_model ();
    }
    
    $conf{mainboard}{vendor} = $vendor;
    $conf{mainboard}{model}  = $model;
}

sub guess_vendor_model_dmidecode {
    my ($vendor, $model);
    my ($system_vendor, $system_model);
    my $line = 0;

    $< == 0 || die "Must be root to run dmidecode\n";

    open (DMI, "$dmidecode |") or die "failed to run $dmidecode: $!\n";

    $vendor = $model = "";

  LINE:
    while (<DMI>) {
        $line++;

        /^(\s*)(board|base board|system) information/i || next LINE;
        my $indent = $1;
	my $type = $2;

        while ( <DMI> ) {
            /^(\s*)/;
            $1 lt $indent && last LINE;
            $indent = $1;
            if ($type eq "system") {
                /(?:manufacturer|vendor):\s*(.*\S)\s*/i && ( $system_vendor = $1 );
                /product(?: name)?:\s*(.*\S)\s*/i       && ( $system_model  = $1 );
            } else {
                /(?:manufacturer|vendor):\s*(.*\S)\s*/i && ( $vendor = $1 );
                /product(?: name)?:\s*(.*\S)\s*/i       && ( $model  = $1 );
            }
            last LINE if ($vendor && $model);
        }
    }

    close (DMI);

    $vendor = $system_vendor if ($vendor eq "");
    $model = $system_model if ($model eq "");

    return ($vendor, $model);
}

sub guess_vendor_model_sysfs {
    #
    #  Try to look up DMI information in sysfs
    #
    open (VENDOR, "/sys/class/dmi/id/board_vendor") or return undef;
    open (MODEL,  "/sys/class/dmi/id/board_name")   or return undef;

    my ($vendor, $model) = (<VENDOR>, <MODEL>);

    close (VENDOR);
    close (MODEL);

    return undef unless ($vendor && $model);

    chomp ($vendor, $model);

    return ($vendor, $model);
}

sub parse_mainboard_config
{
    my ($file) = @_;
    my %hash = ();
    my $line = 0;

    open (CFG, "$file") or die "Failed to read mainboard config: $file: $!\n";
    while (<CFG>) {
        $line++;
        chomp;                                          # remove newline
        s/^((?:[^'"#]*(?:(['"])[^\2]*\2)*)*)#.*/$1/;    # remove comments
        s/^\s+//;                                       # remove leading space
        s/\s+$//;                                       # remove trailing space
        next unless length;                             # skip blank lines
        if (my ($key, $val) = /^\s*([-\w]+)\s*=\s*(.*)/) {
            $hash{$key}{val} = $val;
            $hash{$key}{line} = $line;
            next;
        }
        return undef;
    }
    close (CFG) or &log_error ("close $file: $!\n");
    return \%hash;
}

sub guess_vendor_model {
    my ($vendor, $model);
    #
    #  If mainboard config file exists then parse it 
    #   to get the vendor and model information.
    #
    if (-f $conf{mbconfig} ) {
        my $cfg = &parse_mainboard_config ($conf{mbconfig});

        #  If mainboard config file specified a script, then try to
        #   run the specified script or executable:
        #
        if ($cfg->{"script"}) {
            $cfg = &parse_mainboard_config ("$cfg->{script}{val} |");
            die "Failed to run mainboard script\n" if (!$cfg);
        }
        return ($cfg->{vendor}{val}, $cfg->{model}{val});
    }

    ($vendor, $model) = &guess_vendor_model_sysfs ();

    return ($vendor, $model) if ($vendor && $model);

    return (&guess_vendor_model_dmidecode ());
}

sub parse_dimm_labels_file
{
    my ($lh, $file) = (@_);
    my $line = -1;
    my $vendor = "";
    my @models = ();

    open (LABELS, "$file") 
        or die "Unable to open label database: $file: $!\n";

    while (<LABELS>) {
        $line++;
        next if /^#/;
        chomp; 
        s/^\s+//;
        s/\s+$//;
        next unless length;

        if (/vendor\s*:\s*(.*\S)\s*/i) {
            $vendor = lc $1;
            @models = ();
            next;
        }
        if (/(model|board)\s*:\s*(.*)$/i) {
            !$vendor && die "$file: line $line: MB model without vendor\n";
            @models = grep { s/\s*(.*)\s*$/$1/ } split(/[,;]+/, $2);
            next;
        }

        # Allow multiple labels to be specified on a single line, 
        #  separated by ;
        for my $str (split /;/) {
            $str =~ s/^\s*(.*)\s*$/$1/;

            next unless (my ($label, $info) = ($str =~ /^(.*)\s*:\s*(.*)$/i));

            unless ($info =~ /(\d\.\d\.\d,*)+/) {
                log_error ("$file: $line: Invalid syntax, ignoring: \"$_\"\n");
                next;
            }

            for my $target (split (/[, ]+/, $info)) {
                my ($mc, $row, $chan) = ($target =~ /(\d+)\.(\d+)\.(\d+)/);

                map { $lh->{$vendor}{lc $_}{$mc}{$row}{$chan} = $label } 
                         @models;

            }
        }
    }

    close (LABELS) or die "Error from label db \"$file\" : $!\n";

    return $lh;
}

sub parse_dimm_labels
{
    my %labels = ();

    #
    #  Accrue all DIMM labels from the labels.db file, as
    #   well as any files under the labels dir
    # 
    for my $file ($conf{labeldb}, <$conf{labeldir}/*>) {
    	next unless -r $file;
    	parse_dimm_labels_file (\%labels, $file);
    }

    return \%labels;
}

sub read_dimm_label
{
    my ($mc, $row, $chan) = @_;
    my $sysfs = "/sys/devices/system/edac/mc";

    my $file = "$sysfs/mc$mc/csrow$row/ch${chan}_dimm_label";

    return ("Missing") unless -f $file;

    if (!open (LABEL, "$file")) {
        warn "Failed to open $file: $!\n";
        return ("Error");
    }

    chomp (my $label = <LABEL> || "");

    close (LABEL);

    return ($label);
}

sub print_dimm_labels
{
    my $fh = shift || *STDOUT;
    my $lref = parse_dimm_labels ();
    my $vendor = lc $conf{mainboard}{vendor};
    my $model  = lc $conf{mainboard}{model};
    my $format = "%-35s %-20s %-20s\n";

    if (!exists $$lref{$vendor}{$model}) {
        log_error ("No dimm labels for $conf{mainboard}{vendor} " .
                   "$conf{mainboard}{model}\n");
        return;
    }

    
    printf $fh $format, "LOCATION", "CONFIGURED LABEL", "SYSFS CONTENTS";

    for my $mc (sort keys %{$$lref{$vendor}{$model}}) {
        for my $row (sort keys %{$$lref{$vendor}{$model}{$mc}}) {
            for my $chan (sort keys %{$$lref{$vendor}{$model}{$mc}{$row}}) {

                my $label = $$lref{$vendor}{$model}{$mc}{$row}{$chan};
                my $rlabel = read_dimm_label ($mc, $row, $chan);
                my $loc = "mc$mc/csrow$row/ch${chan}_dimm_label";

                printf $fh $format, $loc, $label, $rlabel;
            }
        }
    }
    print $fh "\n";

}

sub register_dimm_labels
{
    my $lref = parse_dimm_labels ();
    my $vendor = lc $conf{mainboard}{vendor};
    my $model  = lc $conf{mainboard}{model};
    my $sysfs  = "/sys/devices/system/edac/mc";

    if (!exists $$lref{$vendor}{$model}) {
        log_error ("No dimm labels for $conf{mainboard}{vendor} " .
                                      "$conf{mainboard}{model}\n");
        return 0;
    }

    select (undef, undef, undef, $conf{opt}{delay});

    for my $mc (sort keys %{$$lref{$vendor}{$model}}) {
        for my $row (sort keys %{$$lref{$vendor}{$model}{$mc}}) {
            for my $chan (sort keys %{$$lref{$vendor}{$model}{$mc}{$row}}) {

                my $file = "$sysfs/mc$mc/csrow$row/ch${chan}_dimm_label";

                # Ignore sysfs files that don't exist. Might just be
                #  unpopulated bank.
                next unless -f $file;

                if (!open (DL, ">$file")) {
                    warn ("Unable to open $file\n");
                    next;
                }

                syswrite DL, $$lref{$vendor}{$model}{$mc}{$row}{$chan};

                close (DL);

            }
        }
    }
    return 1;
}

sub find_prog
{
    my ($file) = @_;
    for my $dir ("/sbin", "/usr/sbin", split ':', $ENV{PATH}) {
        return "$dir/$file" if -x "$dir/$file";
    }
    # log_error ("Failed to find $file in PATH\n");
    return "";
}

sub log_msg   { print STDERR "$prog: ", @_ unless $conf{opt}{quiet}; }
sub log_error { log_msg ("Error: @_"); }

# vi: ts=4 sw=4 expandtab
