#!/usr/bin/env perl -w
# IBM(c) 2007 EPL license http://www.eclipse.org/legal/epl-v10.html
#####################################################
#
#  xCAT plugin package to handle kernel modules
#
#####################################################

package xCAT_plugin::kmodules;

BEGIN
{
    $::XCATROOT = $ENV{'XCATROOT'} ? $ENV{'XCATROOT'} : '/opt/xcat';
}

require xCAT::Table;
require xCAT::Utils;
require xCAT::TableUtils;
require Data::Dumper;
require Getopt::Long;
require xCAT::MsgUtils;
use File::Path qw(mkpath rmtree);
use File::Basename qw(basename);
use Text::Balanced qw(extract_bracketed);
use Safe;
my $evalcpt = new Safe;

use strict;
use warnings;

#
# Globals
#

#------------------------------------------------------------------------------

=head1    kmodules

This program module file performs kernel module functions

Supported commands:
   lskmodules -- List the kernel modules in one or more:
                    osimage.driverupdatesrc (duds or rpms)
                    kitcomponent.driverpacks (rpms in repodir)
                    osdistro (kernel-<kernvers> rpms)
                    osdistroupdate (kernel-<kernvers> rpms)

=cut

#------------------------------------------------------------------------------

=head2    Kernel Modules Support

=cut

#------------------------------------------------------------------------------

#----------------------------------------------------------------------------

=head3  handled_commands

        Return a list of commands handled by this plugin

=cut

#-----------------------------------------------------------------------------
sub handled_commands {
    return {
        lskmodules => "kmodules"
    };
}

#----------------------------------------------------------------------------

=head3   preprocess_request


        Arguments:

        Returns:
                0 - OK
                1 - error
        Globals:

        Error:

        Example:

        Comments:
=cut

#-----------------------------------------------------------------------------
# sub preprocess_request {
#
# NOT REQUIRED -- no hierarchy for this command
#    my $req = shift;
#    return [$req];
# }

#----------------------------------------------------------------------------

=head3   process_request

        Process the kernel modules commands

        Arguments:

        Returns:
                0 - OK
                1 - error
        Globals:

        Error:

        Example:

        Comments:
=cut

#-----------------------------------------------------------------------------
sub process_request {
    $::request  = shift;
    $::CALLBACK = shift;
    $::SUBREQ   = shift;
    my $ret;

    # globals used by all subroutines.
    $::command   = $::request->{command}->[0];
    $::args      = $::request->{arg};
    $::stdindata = $::request->{stdin}->[0];

    # figure out which cmd and call the subroutine to process
    if ($::command eq "lskmodules") {
        $ret = &lskmodules($::request);
    }

    return $ret;
}

#----------------------------------------------------------------------------

=head3  lskmodules_usage

        Arguments:
        Returns:
        Globals:

        Error:

        Example:

        Comments:
=cut

#-----------------------------------------------------------------------------

# display the usage
sub lskmodules_usage {
    my $rsp;
    push @{ $rsp->{data} },
      "\nUsage: lskmodules - List kernel modules in specified input \n";
    push @{ $rsp->{data} },
"  lskmodules [-V|--verbose] [-x|--xml|--XML] [-c|--kitcomponent kit_comp1,kit_comp2,...] [-o|--osdistro os_distro] [-u|--osdistroupdate os_distro_update] [-i|--osimage osimage]  \n ";
    push @{ $rsp->{data} }, "  lskmodules [-h|--help|-?] \n";
    push @{ $rsp->{data} },
      "  lskmodules [-v|--version]  \n ";
    xCAT::MsgUtils->message("I", $rsp, $::CALLBACK);
    return 0;
}


#----------------------------------------------------------------------------

=head3   processArgs

        Process the command line

        Arguments:

        Returns:
                0 - OK
                1 - just print usage
                2 - error
        Globals:

        Error:

        Example:

        Comments:
=cut

#-----------------------------------------------------------------------------
sub processArgs {

    if (defined($::args) && @{$::args}) {
        @ARGV = @{$::args};
    }

    # parse the options
    # options can be bundled up like -vV, flag unsupported options
    Getopt::Long::Configure("bundling", "no_ignore_case", "no_pass_through");
    Getopt::Long::GetOptions(
        'help|h|?'           => \$::opt_h,
        'kitcomponent|c=s'   => \$::opt_c,
        'osimage|i=s'        => \$::opt_i,
        'osdistro|o=s'       => \$::opt_o,
        'osdistroupdate|u=s' => \$::opt_u,
        'verbose|V'          => \$::opt_V,
        'version|v'          => \$::opt_v,
        'xml|XML|x'          => \$::opt_x,
    );

    # Option -h for Help
    if (defined($::opt_h)) {
        return 2;
    }

    # Option -v for version
    if (defined($::opt_v)) {
        my $rsp;
        my $version = xCAT::Utils->Version();
        push @{ $rsp->{data} }, "$::command - $version\n";
        xCAT::MsgUtils->message("I", $rsp, $::CALLBACK);
        return 1;    # no usage - just exit
    }

    # Option -V for verbose output
    if (defined($::opt_V)) {
        $::verbose = 1;
        $::VERBOSE = 1;
    }

    if (!defined($::opt_c) &&
        !defined($::opt_i) &&
        !defined($::opt_o) &&
        !defined($::opt_u)) {
        my $rsp;
        push @{ $rsp->{data} }, "Specify a search location \n";
        xCAT::MsgUtils->message("I", $rsp, $::CALLBACK);
        return 2;
    }

    my $more_input = shift(@ARGV);
    if (defined($more_input)) {
        my $rsp;
        push @{ $rsp->{data} }, "Invalid input: $more_input \n";
        xCAT::MsgUtils->message("I", $rsp, $::CALLBACK);
        return 2;
    }

    return 0;
}


#----------------------------------------------------------------------------

=head3  lskmodules

        Support for listing kernel modules

        Arguments:
        Returns:
                0 - OK
                1 - error
        Globals:

        Error:

        Example:

        Comments:
=cut

#-----------------------------------------------------------------------------

sub lskmodules {

    my $rc = 0;

    # process the command line
    $rc = &processArgs;
    if ($rc != 0) {

        # rc: 0 - ok, 1 - return, 2 - help, 3 - error
        if ($rc != 1) {
            &lskmodules_usage;
        }
        return ($rc - 1);
    }

    if ($::VERBOSE) {
        my $rsp;
        push @{ $rsp->{data} }, "Running lskmodules command... ";
        xCAT::MsgUtils->message("I", $rsp, $::CALLBACK);
    }

    # Get all the rpms and img files to search based on command input
    my @sources = &set_sources;
    if (!(@sources)) {
        my $rsp;
        push @{ $rsp->{data} }, "No input search source found.";
        xCAT::MsgUtils->message("E", $rsp, $::CALLBACK);
        return 1;
    }

    # Get the list of kernel modules in each rpm/img file
    my @modlist;
    foreach my $source (@sources) {
        if ($source =~ /^dud:/) {
            $source =~ s/^dud://;
            push(@modlist, &mods_in_img($source));
        } else {
            my $osver = xCAT::Utils->osver();
            if ($osver =~ /ubuntu/) {
                $source =~ s/^deb://;
            } else {
                $source =~ s/^rpm://;
            }
            push(@modlist, &mods_in_rpm($source));
        }
    }

    @modlist = &remove_duplicate_mods(\@modlist);

    # Return the module list for this rpm/img file
    my $rsp;
    foreach my $mn (@modlist) {
        if ($::opt_x) {
            my %data_entry;
            $data_entry{module}->{name}        = $mn->{name};
            $data_entry{module}->{description} = $mn->{description};
            push @{ $rsp->{data} }, \%data_entry;
        } else {
            push @{ $rsp->{data} }, $mn->{name} . ': ' . $mn->{description};
        }
    }

    if ($rsp) {
        $::CALLBACK->($rsp);
    }


    return $rc;
}

#----------------------------------------------------------------------------

=head3  set_sources

        return array of input kernel module sources

        Arguments:
        Returns:
                0 - OK
                1 - error
        Globals:

        Error:

        Example:

        Comments:
=cut

#-----------------------------------------------------------------------------

sub set_sources {

    my $installdir = xCAT::TableUtils->getInstallDir;
    my @sources;
    my %src_data;
    my $order = 1;


    # OS images (osimage.driverupdatesrc)
    if (defined($::opt_i)) {
        my $litab = xCAT::Table->new('linuximage');
        foreach my $li (split(',', $::opt_i)) {
            my ($li_entry) = $litab->getAttribs({ 'imagename' => $li }, ('driverupdatesrc'));
            if (!($li_entry)) {
                if ($::VERBOSE) {
                    my $rsp;
                    push @{ $rsp->{data} }, "No driverupdatesrc attribute for osimage $li found.  Skipping. ";
                    xCAT::MsgUtils->message("W", $rsp, $::CALLBACK);
                }
                next;
            }
            foreach my $dus (split(',', $li_entry->{'driverupdatesrc'})) {
                my $base_dus = basename($dus);
                if (defined($src_data{$base_dus})) {
                    if ($::VERBOSE) {
                        my $rsp;
                        push @{ $rsp->{data} }, "Duplicate source $base_dus found in input.  Skipping $dus entry in osimage &li driverupdatesrc.  Using from $src_data{$base_dus}{'sourcemsg'} instead.";
                        xCAT::MsgUtils->message("W", $rsp, $::CALLBACK);
                    }
                    next;
                } else {
                    $src_data{$base_dus}{'sourcemsg'} = "osimage $li driverupdatesrc entry $dus";
                    $src_data{$base_dus}{'source'} = $dus;
                    $src_data{$base_dus}{'order'}  = $order++;
                }
            }
        }
        $litab->close;
    }


    # Kit Components (kitcomp.driverpacks)
    if (defined($::opt_c)) {
        my $kctab = xCAT::Table->new('kitcomponent');
        my $krtab = xCAT::Table->new('kitrepo');
        foreach my $kc (split(',', $::opt_c)) {
            my ($kc_entry) = $kctab->getAttribs({ 'kitcompname' => $kc }, ('kitreponame', 'driverpacks'));
            if (!($kc_entry)) {
                if ($::VERBOSE) {
                    my $rsp;
                    push @{ $rsp->{data} }, "No driverpacks attribute for kitcomponent $kc found.  Skipping.";
                    xCAT::MsgUtils->message("W", $rsp, $::CALLBACK);
                }
                next;
            }
            my ($kr_entry) = $krtab->getAttribs({ 'kitreponame' => $kc_entry->{'kitreponame'} }, ('kitrepodir'));
            if (!($kr_entry)) {
                if ($::VERBOSE) {
                    my $rsp;
                    push @{ $rsp->{data} }, "Kitrepo $kc_entry->{'kitreponame'} not found in database.  Error in kitcomponent definition for $kc.  Skipping.";
                    xCAT::MsgUtils->message("W", $rsp, $::CALLBACK);
                }
                next;
            }
            foreach my $dp (split(',', $kc_entry->{'driverpacks'})) {
                if (defined($src_data{$dp})) {
                    if ($::VERBOSE) {
                        my $rsp;
                        push @{ $rsp->{data} }, "Duplicate source $dp found in input.  Skipping kitcomponent $kc driverpack entry $dp.  Using from $src_data{$dp}{'sourcemsg'} instead.";
                        xCAT::MsgUtils->message("W", $rsp, $::CALLBACK);
                    }
                    next;
                } else {
                    $src_data{$dp}{'sourcemsg'} = "kitcomponent $kc, kitrepo $kc_entry->{'kitreponame'}";
                    $src_data{$dp}{'source'} = $kr_entry->{'kitrepodir'} . '/' . $dp;
                    $src_data{$dp}{'order'} = $order++;
                }
            }
        }
        $kctab->close;
    }

    # OS distro update
    if (defined($::opt_u)) {
        my $outab = xCAT::Table->new('osdistroupdate');
        foreach my $ou (split(',', $::opt_u)) {
            my ($ou_entry) = $outab->getAttribs({ 'osupdatename' => $ou }, ('dirpath', 'basename'));
            if (!($ou_entry) || !(defined($ou_entry->{'dirpath'}))) {
                if ($::VERBOSE) {
                    my $rsp;
                    push @{ $rsp->{data} }, "No dirpath attribute for osdistroupdate $ou found.  Skipping.";
                    xCAT::MsgUtils->message("W", $rsp, $::CALLBACK);
                }
                next;
            }
            my $dirpath     = $ou_entry->{'dirpath'};
            my @kernel_rpms = ();
            if ($ou_entry->{'basename'} =~ /^ubuntu/)
            {
                @kernel_rpms = `find $dirpath/pool/main/l/linux/ -name *deb`;
            }
            else
            {
                @kernel_rpms = `find $dirpath -name kernel-*.rpm`;
            }

            foreach my $krpm (@kernel_rpms) {
                chomp($krpm);
                my $base_krpm = basename($krpm);
                if (defined($src_data{$base_krpm})) {
                    if ($::VERBOSE) {
                        my $rsp;
                        push @{ $rsp->{data} }, "Duplicate source $base_krpm found in input.  Skipping $krpm in osdistroupdate $ou.  Using from $src_data{$base_krpm}{'sourcemsg'} instead.";
                        xCAT::MsgUtils->message("W", $rsp, $::CALLBACK);
                    }
                    next;
                }
                $src_data{$base_krpm}{'sourcemsg'} = "osdistroupdate $ou rpm $krpm";
                $src_data{$base_krpm}{'source'} = $krpm;
                $src_data{$base_krpm}{'order'}  = $order++;
            }
        }
        $outab->close;
    }

    # OS distro
    if (defined($::opt_o)) {
        my $odtab = xCAT::Table->new('osdistro');
        foreach my $od (split(',', $::opt_o)) {
            my ($od_entry) = $odtab->getAttribs({ 'osdistroname' => $od }, ('dirpaths', 'basename'));
            if (!($od_entry)) {

                # try building dirpath from distro_name/local_arch
                my $arch = `uname -m`;
                chomp($arch);
                $arch = "x86" if ($arch =~ /i.86$/);
                my $dirpath = $installdir . '/' . $od . '/' . $arch;
                if (!(-e $dirpath)) {
                    if ($::VERBOSE) {
                        my $rsp;
                        push @{ $rsp->{data} }, "No dirpaths attribute for osdistro $od found.  Skipping. ";
                        xCAT::MsgUtils->message("W", $rsp, $::CALLBACK);
                    }
                    next;
                }

                my @kernel_rpms = ();
                if ($od_entry->{'basename'} =~ /ubuntu/)
                {
                    @kernel_rpms = `find $dirpath/pool/main/l/linux/ -name *deb`;
                }
                else
                {
                    @kernel_rpms = `find $dirpath -name kernel-*.rpm`;
                }

                foreach my $krpm (@kernel_rpms) {
                    chomp($krpm);
                    my $base_krpm = basename($krpm);
                    if (!defined($src_data{$base_krpm})) {
                        $src_data{$base_krpm}{'sourcemsg'} = "osdistro $od default rpm $krpm";
                        $src_data{$base_krpm}{'source'} = $krpm;
                        $src_data{$base_krpm}{'order'}  = $order++;
                    }
                }
            } else {
                foreach my $dirpath (split(',', $od_entry->{'dirpaths'})) {
                    my @kernel_rpms = ();
                    if ($od_entry->{'basename'} =~ /ubuntu/)
                    {
                        @kernel_rpms = `find $dirpath/pool/main/l/linux/ -name *deb`;
                    }
                    else
                    {
                        @kernel_rpms = `find $dirpath -name kernel-*.rpm`;
                    }

                    #my @kernel_rpms = `find $dirpath -name kernel-*.rpm`;
                    foreach my $krpm (@kernel_rpms) {
                        chomp($krpm);
                        my $base_krpm = basename($krpm);
                        if (defined($src_data{$base_krpm})) {
                            if ($::VERBOSE) {
                                my $rsp;
                                push @{ $rsp->{data} }, "Duplicate source $base_krpm found in input.  Skipping $krpm in osdistro $od.  Using from $src_data{$base_krpm}{'sourcemsg'} instead.";
                                xCAT::MsgUtils->message("W", $rsp, $::CALLBACK);
                            }
                            next;
                        }
                        $src_data{$base_krpm}{'sourcemsg'} = "osdistro $od rpm $krpm";
                        $src_data{$base_krpm}{'source'} = $krpm;
                        $src_data{$base_krpm}{'order'}  = $order++;
                    }
                }
            }
        }
        $odtab->close;
    }



    foreach my $base_source (sort { $src_data{$a}{'order'} <=> $src_data{$b}{'order'} } keys %src_data) {
        push @sources, $src_data{$base_source}{'source'};
    }

    if ($::VERBOSE && @sources) {
        my $rsp;
        push @{ $rsp->{data} }, "Searching the following locations for kernel modules: ";
        push @{ $rsp->{data} }, @sources;
        xCAT::MsgUtils->message("I", $rsp, $::CALLBACK);
    }


    return @sources;
}

#----------------------------------------------------------------------------

=head3  mods_in_rpm

        return hash of module names/descriptions found in rpm

        Arguments:
        Returns:
                0 - OK
                1 - error
        Globals:

        Error:

        Example:

        Comments:
=cut

#-----------------------------------------------------------------------------

sub mods_in_rpm {

    my $krpm = shift;
    my @modlist;


    my $tmp_path = "/tmp/lskmodules_expanded_rpm";
    mkpath($tmp_path);
    if (-r $krpm) {
        my $osver = xCAT::Utils->osver();
        if ($osver =~ /ubuntu/) {
            if (system("cd $tmp_path; dpkg -X $krpm $tmp_path > /dev/null 2>&1 ; cd - > /dev/null 2>&1")) {
                my $rsp;
                push @{ $rsp->{data} }, "Unable to extract files from the deb package $krpm.";
                xCAT::MsgUtils->message("E", $rsp, $::CALLBACK);
                rmtree($tmp_path);
                return; }
        } else {
            if (system("cd $tmp_path; rpm2cpio $krpm | cpio -idum  > /dev/null 2>&1 ; cd - > /dev/null 2>&1")) {
                my $rsp;
                push @{ $rsp->{data} }, "Unable to extract files from the rpm $krpm.";
                xCAT::MsgUtils->message("E", $rsp, $::CALLBACK);
                rmtree($tmp_path);
                return;
            }
        }
    } else {
        my $rsp;
        push @{ $rsp->{data} }, "Unable to read rpm $krpm.";
        xCAT::MsgUtils->message("E", $rsp, $::CALLBACK);
        rmtree($tmp_path);
        return;
    }

    my @ko_files = `find $tmp_path -regextype posix-egrep  -regex ".*/*\.ko(\.xz)?"`;
    foreach my $ko (@ko_files) {
        my %mod;
        chomp($ko);
        my $name = basename($ko);
        my $desc = `modinfo -d $ko`;
        chomp($desc);
        if ($desc =~ /^\s*$/) {
            $desc = "  ";
        }
        $mod{name}        = $name;
        $mod{description} = $desc;
        push(@modlist, \%mod);
    }
    rmtree($tmp_path);

    return @modlist;

}

#----------------------------------------------------------------------------

=head3  mods_in_img

        return hash of module names/descriptions found in
           driver update image

        Arguments:
        Returns:
                0 - OK
                1 - error
        Globals:

        Error:

        Example:

        Comments:
=cut

#-----------------------------------------------------------------------------

sub mods_in_img {

    my $img_file = shift;
    my @modlist;

    my $mnt_path = "/tmp/lskmodules_mnt";
    mkpath($mnt_path);

    my $rc = system("mount -o loop $img_file $mnt_path");
    if ($rc) {
        my $rsp;
        push @{ $rsp->{data} }, "Mount of driver disk image $img_file failed";
        xCAT::MsgUtils->message("E", $rsp, $::CALLBACK);
        rmtree($mnt_path);
        return;
    }

    my @ko_files = `find $mnt_path -name *.ko`;
    foreach my $ko (@ko_files) {
        my %mod;
        chomp($ko);
        my $name = basename($ko);
        my $desc = `modinfo -d $ko`;
        chomp($desc);
        if ($desc =~ /^\w*$/) {
            $desc = "  ";
        }
        $mod{name}        = $name;
        $mod{description} = $desc;
        push(@modlist, \%mod);
    }

    $rc = system("umount $mnt_path");
    rmtree($mnt_path);

    return @modlist;
}



#----------------------------------------------------------------------------

=head3  remove_duplicate_mods

        return array of unique module names/descriptions hashes from input array

        Arguments:
        Returns:
                0 - OK
                1 - error
        Globals:

        Error:

        Example:

        Comments:
=cut

#-----------------------------------------------------------------------------

sub remove_duplicate_mods {

    my $modlist = shift;

    my %unique_names;
    my @return_list;

    foreach my $mn (@$modlist) {
        if (defined($unique_names{ $mn->{name} })) { next; }
        $unique_names{ $mn->{name} } = $mn->{description};
    }

    foreach my $un (sort keys(%unique_names)) {
        my %mod;
        $mod{name}        = $un;
        $mod{description} = $unique_names{$un};
        push(@return_list, \%mod);
    }

    return @return_list;
}

1;
