#!/usr/bin/perl

#
# FCP report generators
#
# Script to list the FCP, SCSI and multipath configuration
#
# Copyright IBM Corp. 2008, 2024
#
# s390-tools is free software; you can redistribute it and/or modify
# it under the terms of the MIT license. See LICENSE for details.
#

use strict;
use warnings;
use English;
use Getopt::Long;
use File::Spec::Functions;
use File::Temp qw/ tempdir /;
use Data::Dumper;

use constant S_DIR1 => "/sys/class/fc_host";
use constant S_DIR2 => "/sys/devices/css0"; #/<sub_ch>/<device>
use constant S_DIR3 => "/sys/class/scsi_device";
use constant S_DIR4 => "/sys/class/fc_remote_ports";

our $base_dir = "/";
our %sub_ch;
our %devices;
our %mapper_dev;
our %rports;
our $c_src;

sub get_line
{
	return "n/a" unless open(FH, (catfile($c_src, @_))[0]);
	my $line = readline(FH);
	$line =~ s/\s*\Z// unless !$line;
	close(FH);
	return $line;
}

sub dir_content
{
	my @temp_dir;
	opendir(DH, shift()) or return 0;
	@temp_dir = readdir(DH);
	closedir(DH);
	return @temp_dir;
}

sub glob_last
{
	return (glob(catfile($c_src, @_)))[-1];
}

sub mm_to_32
{
	return "0" unless ($_[0] and $_[0] =~ /\d+:\d+/);
	my @t_arr = split(':',shift);
	return ($t_arr[0] << 20) + $t_arr[1];
}

sub get_sub_ch_data
{
	my @entries = grep { /^host\d+\Z/ }
			dir_content(catdir($base_dir, S_DIR1));

	foreach my $host (@entries) {
		$c_src = catdir($base_dir, S_DIR1, $host);
		my $sub_ch;
		my $adapter;
		my $chp;
		if (-l $c_src) {
			$sub_ch = (split("/", readlink($c_src)))[-5];
			$adapter = (split("/", readlink($c_src)))[-4];
		} else {
			$sub_ch = (split("/", readlink($c_src."/device")))[-3];
			$adapter = (split("/", readlink($c_src."/device")))[-2];
		}
		$sub_ch{$adapter}{sub_ch} = $sub_ch;
		$sub_ch{$adapter}{host}  = $host;
		$sub_ch{$adapter}{speed} = get_line("speed");
		$sub_ch{$adapter}{name}  = get_line("port_name");
		$sub_ch{$adapter}{id}    = get_line("port_id");
		$sub_ch{$adapter}{pname} = get_line("permanent_port_name");
		$sub_ch{$adapter}{type}  = get_line("port_type");
		$c_src = catdir($base_dir, S_DIR2, $sub_ch, $adapter);
		$sub_ch{$adapter}{lic}   = get_line("lic_version");
		$sub_ch{$adapter}{faild} = get_line("failed") == 0 ? "No" :
	                                                             "Yes";
		$sub_ch{$adapter}{gen}   = get_line("card_version");
		$sub_ch{$adapter}{state} = get_line("online") == 1 ? "Online" :
								     "Offline";
		$c_src = catdir($base_dir, S_DIR2, $sub_ch);
		$sub_ch{$adapter}{chpid} = substr(get_line("chpids"), 0, 2);
		$chp = "chp0." . $sub_ch{$adapter}{chpid};
		$c_src = catdir($base_dir, S_DIR2, $chp);
		if (-e catdir($base_dir, S_DIR2, $chp, "chid")) {
		    $sub_ch{$adapter}{pchid} = get_line("chid");
		} else {
		    $sub_ch{$adapter}{pchid} = "n/a"
		}
	}
}

sub get_device_data
{
	my @entries = grep { /^\d+:\d+:\d+:\d+\Z/ }
			dir_content(catdir($base_dir, S_DIR3));

	foreach my $hctl (@entries) {
		$c_src = catdir($base_dir, S_DIR3, $hctl, "device");
		my $type =  get_line("type");
		$devices{$hctl}{hba_id} = get_line("hba_id");
		$devices{$hctl}{model}  = get_line("model");
		$devices{$hctl}{vendor} = get_line("vendor");
		$devices{$hctl}{lun}    = get_line("fcp_lun");
		$devices{$hctl}{wwpn}   = get_line("wwpn");
		$devices{$hctl}{sg} = (-d catdir($c_src, "scsi_generic")) ?
					glob_last(catdir("scsi_generic/*")) :
					glob_last("scsi_generic\:*");

		$devices{$hctl}{sg}     =~ s/.*(sg\d+)\Z/$1/
					unless(!$devices{$hctl}{sg});
		$devices{$hctl}{type}   = "type_".$type; #preset only
		$devices{$hctl}{mm}     = "0:0"; #preset only
		$devices{$hctl}{sg_mm}  = "0:0"; #preset only
		$devices{$hctl}{dev}    = undef; #preset only
		if ($type == "0") {
			$devices{$hctl}{type} = "Disk";
			if (-d catdir($c_src, "block")) {
				$devices{$hctl}{dev}  = glob_last("block/sd*");
			} else {
				$devices{$hctl}{dev}  = glob_last("block\:sd*");
			}
			$devices{$hctl}{sg_mm}   = get_line("generic", "dev");
			next if !defined $devices{$hctl}{dev};
			$c_src = $devices{$hctl}{dev};
			$devices{$hctl}{dev}  =~ s/.*(sd[a-z0-9]+)\Z/$1/
						unless(!$devices{$hctl}{dev});
			$devices{$hctl}{mm}   = get_line("dev");
			$devices{$hctl}{mp_dev} = glob_last("holders", "dm*");
			next if !defined $devices{$hctl}{mp_dev};
			$devices{$hctl}{mp_dev} =~ s/.*\/(.+)\Z/$1/;
			$c_src = catdir($base_dir, "sys", "block",
					$devices{$hctl}{mp_dev});
			$devices{$hctl}{mp_dev_mm} = get_line("dev");
		} elsif ($type == "1") {
			$devices{$hctl}{type} = "Tape";
			$devices{$hctl}{mm} = get_line("tape", "dev");
			$devices{$hctl}{sg_mm} = get_line("generic", "dev");
			$devices{$hctl}{dev} = (-d catdir($c_src, "scsi_tape")) ?
						(grep { /st\d+\Z/ }
                                                glob(catdir($c_src, "scsi_tape/st*")))[0] :
						(grep { /\:st\d+\Z/ }
						glob(catdir($c_src, "scsi_tape:st*")))[0];
			$devices{$hctl}{dev} =~ s/.*(st\d+)\Z/$1/
						unless(!$devices{$hctl}{dev});
		}
	}
}

sub get_mapper_devices
{
	$c_src = catdir($base_dir, "dev", "mapper");
	my @entries = grep { ! /^\./ } dir_content($c_src);

	foreach my $dev (@entries) {
		next if ("control" =~ /$dev/);
		if ($base_dir !~ /^\/\Z/) {
			$mapper_dev{get_line("$dev")} = $dev;
			next;
		}
		my $tf = catfile($c_src, "$dev");
		my $mm = `stat -L -c%t:%T $tf`;
		chomp($mm);
		$mm = join(":", map { hex($_) } split(":", $mm));
		$mapper_dev{$mm} = $dev;
	}
}

sub get_rport_data
{
	my @entries = grep { ! /^\./ } dir_content(catdir($base_dir, S_DIR4));

	foreach my $rport (@entries) {
		$c_src = catdir($base_dir, S_DIR4, "$rport");
		$rports{$rport}{wwpn} = get_line("port_name");
		$rports{$rport}{did} = get_line("port_id");
		$rports{$rport}{sup_class} = get_line("supported_classes");
		$rports{$rport}{scsi_target} = get_line("scsi_target_id");
		$rports{$rport}{dl_tmo} = get_line("dev_loss_tmo");
		$rports{$rport}{state} = get_line("port_state");
		$rports{$rport}{hba_id} = (split('/', get_line("uevent")))[-1];
	}
}

sub mapping_table
{
	foreach my $line (sort keys %devices) {
		my @out_str;
		my $mp_dev_mm = $devices{$line}{mp_dev_mm};

		push @out_str, $sub_ch{$devices{$line}{hba_id}}{chpid};
		push @out_str, $line;
		push @out_str, "$sub_ch{$devices{$line}{hba_id}}{sub_ch}:".
			       "$devices{$line}{hba_id}:$devices{$line}{wwpn}:".
				$devices{$line}{lun};
		if ($mp_dev_mm && defined $mapper_dev{$mp_dev_mm}) {
			push @out_str, "/dev/mapper/$mapper_dev{$mp_dev_mm}";
			push @out_str, mm_to_32($devices{$line}{mp_dev_mm});
			push @out_str, "$devices{$line}{mp_dev_mm}";
		} else {
			push @out_str, "n/a";
			push @out_str, "0";
			push @out_str, "0:0";
		}
		if ($devices{$line}{type} eq "Tape") {
			push @out_str, $devices{$line}{dev} ? "/dev/$devices{$line}{sg}" : "n/a" ;
			push @out_str, mm_to_32($devices{$line}{sg_mm});
			push @out_str, $devices{$line}{sg_mm};
		} else {
			push @out_str, $devices{$line}{dev} ? "/dev/$devices{$line}{dev}" : "n/a" ;
			push @out_str, mm_to_32($devices{$line}{mm});
			push @out_str, $devices{$line}{mm};
		}
		push @out_str, $devices{$line}{type};
		push @out_str, $sub_ch{$devices{$line}{hba_id}}{pchid};
		print "@out_str\n";
	}
}

sub adapter_report
{
	my @adapters = @_;

	foreach my $a (sort keys %sub_ch) {
		next if (@adapters && "@adapters" !~ /\b$a\b/);
		my @out_str;
		push @out_str, "Host:    $sub_ch{$a}{host}\n";
		if ($sub_ch{$a}{pchid} eq "n/a") {
			push @out_str, "PCHID:   there is no PCHID entry in data source\n";
		} else {
			push @out_str, "PCHID:   $sub_ch{$a}{pchid}\n";
		}
		push @out_str, "CHPID:   $sub_ch{$a}{chpid}\n";
		push @out_str, "Adapter: $a\n";
		push @out_str, "Sub-Ch.: $sub_ch{$a}{sub_ch}\n";
		push @out_str, "Name:    $sub_ch{$a}{name}\n";
		push @out_str, "Port-Id: $sub_ch{$a}{id}\n";
		push @out_str, "P-Name:  $sub_ch{$a}{pname}\n";
		push @out_str, "Version: $sub_ch{$a}{gen}\n";
		push @out_str, "LIC:     $sub_ch{$a}{lic}\n";
		push @out_str, "Type:    $sub_ch{$a}{type}\n";
		push @out_str, "Speed:   $sub_ch{$a}{speed}\n";
		push @out_str, "State:   $sub_ch{$a}{state}\n";
		push @out_str, "Failed:  $sub_ch{$a}{faild}\n";
		print @out_str, "\n";
	}
}

sub device_report
{
	my $adapters = shift;
	my $ports = shift;
	my $luns = shift;
	my $s_devs = shift;
	my $hosts = shift;

	if ($_[0]) {
		print "adapter  remote_port        LUN                SCSI  ",
		      "gen_dev  scsi_dev MM  type model vendor H:C:T:L\n",
		      "======================================================",
		      "===============================================\n";
	}
	foreach my $hctl (sort keys %devices) {
		next if (@$adapters && "@$adapters" !~ /\b$devices{$hctl}{hba_id}\b/);
		next if (@$ports && "@$ports" !~ /\b$devices{$hctl}{wwpn}\b/);
		next if (@$luns && "@$luns" !~ /\b$devices{$hctl}{lun}\b/);
		next if (@$s_devs && "@$s_devs" !~ /\b$devices{$hctl}{dev}\b/);
		next if (@$hosts && "@$hosts" !~ /\b$sub_ch{$devices{$hctl}{hba_id}}{host}\b/);
		my @out_str;
		push @out_str, $devices{$hctl}{hba_id};
		push @out_str, $devices{$hctl}{wwpn};
		push @out_str, $devices{$hctl}{lun};
		push @out_str, $sub_ch{$devices{$hctl}{hba_id}}{host};
		push @out_str, $devices{$hctl}{sg} ?
					"/dev/".$devices{$hctl}{sg}: "NO_SG";
		push @out_str, $devices{$hctl}{dev} ?
					"/dev/".$devices{$hctl}{dev}: "NO_SCSI";
		push @out_str, $devices{$hctl}{mm};
		push @out_str, $devices{$hctl}{type};
		push @out_str, $devices{$hctl}{model};
		push @out_str, $devices{$hctl}{vendor};
		push @out_str, $hctl;

		print "@out_str\n";
	}
}

sub mapper_report
{
	my $adapters = shift;
	my $ports = shift;
	my $luns = shift;
	my $s_devs = shift;
	my $m_devs = shift;
	my @out_str;

	if (! (keys %mapper_dev)) {
		print "No device mapper devices configured.\n";
		return;
	}
	if ($_[0]) {
		print "adapter  remote_port        LUN                scsi_dev multipath_device\n",
		      "========================================================================\n";
	}
	foreach my $hctl (sort keys %devices) {
		next if (!defined $devices{$hctl}{mp_dev});
		next if (@$adapters && "@$adapters" !~ /\b$devices{$hctl}{hba_id}\b/);
		next if (@$ports && "@$ports" !~ /\b$devices{$hctl}{wwpn}\b/);
		next if (@$luns && "@$luns" !~ /\b$devices{$hctl}{lun}\b/);
		next if (@$s_devs && "@$s_devs" !~ /\b$devices{$hctl}{dev}\b/);
		next if (@$m_devs && "@$m_devs" !~ /\b$mapper_dev{$devices{$hctl}{mp_dev_mm}}\b/);
		my @line_str;
		push @line_str, $devices{$hctl}{hba_id};
		push @line_str, $devices{$hctl}{wwpn};
		push @line_str, $devices{$hctl}{lun};
		push @line_str, "/dev/".$devices{$hctl}{dev};
		push @line_str, "/dev/mapper/".$mapper_dev{$devices{$hctl}{mp_dev_mm}};
		push @out_str, "@line_str\n";
	}
	@out_str = sort {
			my @a_fields = split('\s+', $a);
			my @b_fields = split('\s+', $b);

			$a_fields[3] cmp $b_fields[3]
				||
			$a_fields[1] cmp $b_fields[1]
				||
			$a_fields[0] cmp $b_fields[0]
		} @out_str;
	print @out_str;
}

sub usage
{
    print <<MSG
Usage: $PROGRAM_NAME [-h] [-v] [-t] [-i <source>] [-a <adapter>]
                     [-p <port>] [-l <lun>] [-s <host>] [-d <dev>] [-m <dev>]
                     [-A] [-D] [-M] [-I]

$PROGRAM_NAME provides information of the multipath, SCSI and FCP configuration

The default is to print the adapter configuration report.

Options:

        -h, --help
            print this help text.

        -v, --version
            print version information.

        -t, --topline
            print a header for column description
            the default is to print no header which is useful if the results
            are imported by another application.
            e.g. $PROGRAM_NAME -D -t

        -i, --input <source>
            specify a directory or configuration file as source
            instead of working on the current live-system.

        -a, --adapter <adapter>
            limit the output to the list of adapters specified
            e.g. $PROGRAM_NAME -a 0.0.3c07,0.0.3d07

        -p, --port <port>
            limit the output to the list of remote-ports specified
            e.g. $PROGRAM_NAME -D -p 0x5005123456789000,0x5005123456789001

        -l, --lun <lun>
            limit the output to the list of LUNs specified
            e.g. $PROGRAM_NAME -D -l 0x401040a600000000 -l 0x401040a700000000

        -s, --scsi <host>
            limit the output to the list SCSI hosts specified
            e.g. $PROGRAM_NAME -D --scsi host0,host1 -s host5

        -d, --device <dev>
            limit the output to the list of SCSI devices specified
            e.g. $PROGRAM_NAME -D --device sda,sdb -d sde

        -m, --mdev <dev>
            limit the output to the list of multipath devices specified
            e.g. $PROGRAM_NAME -M -m 36005076303ffc56200000000000010a6

        -A, --Adapter
            print the adapter report, this is the default.

        -D, --Device
            print the device report

        -M, --Map
            print the multipath, mapper report

        -I, --Internal
            prints an assembly of data used as input for a variety of reports
MSG
}

sub version {
	print "$PROGRAM_NAME: version %S390_TOOLS_VERSION%\n";
	print "Copyright IBM Corp. 2008, 2024\n";
}

sub unpack_config
{
	my $td = tempdir( CLEANUP => 1 );

	print STDERR "Unpacking configuration ...";
	system("tar xzf $base_dir -C $td 2>/dev/null");
	print STDERR "Done.\n";

	$base_dir = $td;
}

sub verify_and_init
{
	# only brief checks if there's something at all #
	return -1 if (! (-e catdir($base_dir, S_DIR1) &&
			 -e catdir($base_dir, S_DIR2) &&
			 -e catdir($base_dir, S_DIR3) &&
			 -e catdir($base_dir, S_DIR4)));

	get_sub_ch_data($base_dir);
	get_device_data($base_dir);
	get_mapper_devices($base_dir);
	#get_rport_data($base_dir);

	return 0;
}

##################### main #######################
my @adapter;
my @port;
my @lun;
my @host;
my @s_dev;
my @m_dev;
my $int_map_list;
my $adapter_rep;
my $device_rep;
my $map_rep;
my $header = 0;

Getopt::Long::Configure(qw/bundling no_ignore_case/);
GetOptions('i|input=s' => \$base_dir,
           'a|adapter=s' => \@adapter,
           't|topline' => \$header,
           'h|help' => sub { usage(); exit 0; },
           'v|version' => sub { version(); exit 0; },
           'p|port=s' => \@port,
           'l|lun=s' => \@lun,
           's|scsi=s' => \@host,
           'd|device=s' => \@s_dev,
           'm|mdev=s' => \@m_dev,
           'I|Internal' => \$int_map_list,
           'A|Adapter' => \$adapter_rep,
           'D|Device' => \$device_rep,
           'M|Map' => \$map_rep,
          ) or do {
	print "Invalid usage !\n";
	usage();
	exit 1;
};

if (scalar(@ARGV)) {
	print "$PROGRAM_NAME: extra operand '@ARGV'\n";
	print "Try '$PROGRAM_NAME --help' for more information.\n";
	exit 2;
}

if (-f $base_dir) {
	unpack_config();
}

if (verify_and_init() != 0) {
	print "No valid configuration found.\nTerminating\n\n";
	exit 4;
}

@adapter = split(',', join(',', @adapter));
@port = split(',', join(',', @port));
@lun = split(',', join(',', @lun));
@host = split(',', join(',', @host));
@s_dev = split(',', join(',', @s_dev));
@m_dev = split(',', join(',', @m_dev));

if ($int_map_list) {
	mapping_table();
	exit 0;
}

if ($adapter_rep || !($device_rep || $map_rep)) {
	adapter_report(@adapter);
}

if ($device_rep) {
	device_report(\@adapter, \@port, \@lun, \@s_dev, \@host, $header);
}

if ($map_rep) {
	mapper_report(\@adapter, \@port, \@lun, \@s_dev, \@m_dev, $header);
}

