#!/usr/bin/perl
#
# Copyright (c) 2018-2019, AT&T Intellectual Property. All rights reserved.
#
# SPDX-License-Identifier: GPL-2.0-only
#

# The entity sensor trap service monitors IPMI SEL for the last sel
# event, and if a new event occurs, sends trap varbindlist values
# to snmp sensortrap subagent. Traps are sent for all threshold tyoe
# sensor records logged since the last poll.
#
# ipmitool raw command is used to get the SEL record, as the response
# of this command contains the next record ID, which is used in fetching
# records logged since the last poll. The raw command format is as follows:
#
# ipmitool raw <netfn> <cmd> <data>
#
# Network function for retrieving SEL records from non-volatile storage is:
# 0x0a (storage)
# and SEL get command is: 0x43.
#
# ipmi get sel record request:
#
# ipmitool raw 0x0a 0x43 D[1:2] D[3:4] D[5] D[6]
# Byte [1:2] Reservation ID
#           LS Byte first
#           Only required for partial Get
#           Use 0000h otherwise
# Byte [3:4] SEL Record ID
#           LS Byte first
#           0000h = GET FIRST ENTRY
#           FFFFh = GET LAST ENTRY
# Byte 5     Offset Into Record
# Byte 6     Bytes to Read
#           FFh means read entire record
#
# Examples:
#
# Get last record:
#
#root@siad-b35:/home/vyatta# ipmitool raw 0x0a 0x43 0x00 0x00 0xff 0xff 0x00 0xff
# ff ff 38 0e 02 4a ae ff 4e 20 00 04 02 ed 81 59
# fe ff
#
# Get record ID 0x0e37:
#
#root@siad-b35:/home/vyatta# ipmitool raw 0x0a 0x43 0x00 0x00 0x37 0x0e 0x00 0xff
# 38 0e 37 0e 02 4a ae ff 4e 20 00 04 02 ed 01 58
# fe ff
#
# The first two bytes of the response are the LS amd MS bytes
# of the next SEL record ID, and the following two bytes are the
# LS amd MS bytes of the record ID requested.
#
# Please refer to IPMI specification for more details.

use strict;
use warnings;

use Sys::Syslog qw(:standard :macros);

my $reset_last_id    = 0;
my $sel_count        = 0;
my $sel_last_id      = "0000";
my $sel_end_id       = "ffff";
my $ipmi_get_sel_rec = "ipmitool raw 0x0a 0x43 0x00 0x00";
my $rec_offset_len   = "0x00 0xff";

my @IPMI_DEVICES = ( '/dev/ipmi0', '/dev/ipmi/0', '/dev/ipmidev/0' );

my $TRAP_CMD = "/usr/bin/agentxtrap";
my $INTERVAL = 10;

my $NOTIFICATIONS_ARGS_FILE = "/run/snmpd/vyatta-entity-state-traps-args";
my $SELMGMT_ARGS_FILE       = "/run/snmpd/selmgmt-args";

my $SEL_PERCENTUSED_THRESHOLD = 90;
my $SEL_FULL                  = 100;
my $SEL_THRESHOLD_WARNING =
  "BMC System Event Log (SEL) is over $SEL_PERCENTUSED_THRESHOLD% full";
my $SEL_FULL_ALERT =
  "BMC System Event Log (SEL) is full; new messages are not logged to SEL";

my $selmgmt_mode   = "capacity";
my $syslog_enabled = 0;

my %SensorPrecision = (
    'Voltage'     => 3,
    'Temperature' => 2,
    'Fan'         => 0,
);

my %physicalClass = (
    'other'       => 1,
    'unknown'     => 2,
    'chassis'     => 3,
    'backplane'   => 4,
    'container'   => 5,
    'powerSupply' => 6,
    'fan'         => 7,
    'sensor'      => 8,
    'module'      => 9,
    'port'        => 10,
    'stack'       => 11,
    'cpu'         => 12,
);

my %SELSensorType = (
    'Power Supply'    => 'powerSupply',
    'Fan'             => 'fan',
    'Processor'       => 'cpu',
    'Microcontroller' => 'cpu',
);

my %EntityStateAdmin = (
    'unknown'      => 1,
    'locked'       => 2,
    'shuttingDown' => 3,
    'unlocked'     => 4,
);

my %EntityStateOper = (
    'unknown'  => 1,
    'disabled' => 2,
    'enabled'  => 3,
    'testing'  => 4,
);

my %EntityStateAlarm = (
    'unknown'       => 0,
    'underRepair'   => 1,
    'critical'      => 2,
    'major'         => 3,
    'minor'         => 4,
    'warning'       => 5,
    'indeterminate' => 6,
);

my %thresholdRising = (
    'low'  => '<',
    'high' => '>',
);

my $entConfigChange = "1.3.6.1.2.1.47.2.0.1";

my $entPhysicalTable       = "1.3.6.1.2.1.47.1.1.1";
my $entPhysicalEntry       = "$entPhysicalTable.1";
my $entPhysicalIndex       = "$entPhysicalEntry.1";
my $entPhysicalDescr       = "$entPhysicalEntry.2";
my $entPhysicalContainedIn = "$entPhysicalEntry.4";
my $entPhysicalClass       = "$entPhysicalEntry.5";

my $entSensorThresholdNotifications  = "1.3.6.1.4.1.74.1.32.2.0";
my $entSensorThresholdNonRecoverable = "$entSensorThresholdNotifications.1";
my $entSensorThresholdCritical       = "$entSensorThresholdNotifications.2";
my $entSensorThresholdNonCritical    = "$entSensorThresholdNotifications.3";

my $entSensorThresholdObjects   = "1.3.6.1.4.1.74.1.32.2.1";
my $entSensorThresholdTable     = "$entSensorThresholdObjects.1";
my $entSensorThresholdEntry     = "$entSensorThresholdTable.1";
my $entSensorTriggerReading     = "$entSensorThresholdEntry.7";
my $entSensorTriggerThreshold   = "$entSensorThresholdEntry.8";
my $entSensorTriggerDescription = "$entSensorThresholdEntry.9";

my $entStateTable       = "1.3.6.1.2.1.131.1.1";
my $entStateEntry       = "$entStateTable.1";
my $entStateLastChanged = "$entStateEntry.1";
my $entStateAdmin       = "$entStateEntry.2";
my $entStateOper        = "$entStateEntry.3";
my $entStateAlarm       = "$entStateEntry.5";

my $entStateNotifications = "1.3.6.1.2.1.131.0";
my $entStateOperEnabled   = "$entStateNotifications.1";
my $entStateOperDisabled  = "$entStateNotifications.2";

my $entStateExtNotifications = "1.3.6.1.4.1.74.1.32.3.0";
my $entStatusChange          = "$entStateExtNotifications.1";
my $entFRUInserted           = "$entStateExtNotifications.2";
my $entFRURemoved            = "$entStateExtNotifications.3";

my $send_entity_sensor_traps = 0;
my $send_entity_state_traps  = 0;

my %last_saved_state = ();

my $fh2;

sub debug {
    my @msg = @_;
    print $fh2 scalar localtime(), ":  ", @msg, "\n";
    return;
}

sub trim {
    my ($string) = @_;

    $string =~ s/^\s+|\s+$//g;

    return $string;
}

sub get_config_change_varbinds {
    my ($einfo) = @_;
    return if ( !defined($einfo) );

    my @varbinds;
    push @varbinds, $entConfigChange;

    my $id = $einfo->{'Sensor Number'};
    return if ( !defined($id) );
    push @varbinds, "$entPhysicalIndex i ${\hex($id)}";

    my $sensor_name = $einfo->{'Sensor ID'};
    if ( defined($sensor_name) ) {
        my ( $name, undef ) = split / /, $sensor_name, 2;
        push @varbinds, "$entPhysicalDescr s '$name'";
    }

    return join( ' ', @varbinds );
}

sub get_status_change_varbinds {
    my ( $einfo, $descr ) = @_;
    return if ( !defined($einfo) || !defined($descr) );

    my $state_oper;
    if ( $descr =~ /Present/ ) {
        $state_oper = $EntityStateOper{'enabled'};
    }
    elsif ( $descr =~ /Absent/ ) {
        $state_oper = $EntityStateOper{'disabled'};
    }
    else {
        return;
    }

    my @varbinds;
    push @varbinds, $entStatusChange;

    my $id = $einfo->{'Sensor Number'};
    return if ( !defined($id) );
    push @varbinds, "$entPhysicalIndex i ${\hex($id)}";

    my $sensor_name = $einfo->{'Sensor ID'};
    if ( defined($sensor_name) ) {
        my ( $name, undef ) = split / /, $sensor_name, 2;
        push @varbinds, "$entPhysicalDescr s '$name'";
    }

    my $type  = $einfo->{'Sensor Type'};
    my $class = $physicalClass{'other'};
    $class = $physicalClass{ $SELSensorType{$type} } if ( defined($type) );
    push @varbinds, "$entPhysicalClass i $class";

    push @varbinds, "$entStateOper i $state_oper";
    push @varbinds, "$entStateAdmin i $EntityStateAdmin{'unlocked'}";

    my $last_changed = $einfo->{'Timestamp'};
    my @datetime = split( / /, $last_changed );
    if (@datetime) {
        my @date = split( /\//, $datetime[0] );
        my @time = split( /:/,  $datetime[1] );
        if ( @date && @time ) {
            $last_changed = sprintf(
                "%2d-%1d-%1d,%1d:%1d:%1d",
                int( $date[2] ),
                int( $date[1] ),
                int( $date[0] ),
                int( $time[0] ),
                int( $time[1] ),
                int( $time[2] )
            );
        }
    }
    push @varbinds, "$entStateLastChanged s $last_changed";

    return join( ' ', @varbinds );
}

sub get_fru_varbinds {
    my ( $einfo, $descr ) = @_;
    return if ( !defined($einfo) || !defined($descr) );

    my $trap_oid;
    if ( $descr =~ /Present/ ) {
        $trap_oid = $entFRUInserted;
    }
    elsif ( $descr =~ /Absent/ ) {
        $trap_oid = $entFRURemoved;
    }
    else {
        return;
    }

    my @varbinds;
    push @varbinds, $trap_oid;

    my $id = $einfo->{'Sensor Number'};
    return if ( !defined($id) );
    push @varbinds, "$entPhysicalIndex i ${\hex($id)}";

    my $sensor_name = $einfo->{'Sensor ID'};
    if ( defined($sensor_name) ) {
        my ( $name, undef ) = split / /, $sensor_name, 2;
        push @varbinds, "$entPhysicalDescr s '$name'";
    }

    my $type  = $einfo->{'Sensor Type'};
    my $class = $physicalClass{'other'};
    $class = $physicalClass{ $SELSensorType{$type} } if ( defined($type) );
    push @varbinds, "$entPhysicalClass i $class";
    push @varbinds, "$entPhysicalContainedIn i 0";

    return join( ' ', @varbinds );
}

sub get_state_oper_varbinds {
    my ( $einfo, $descr ) = @_;
    return if ( !defined($einfo) || !defined($descr) );

    my $trap_oid;
    if ( $descr =~ /Present/ ) {
        $trap_oid = $entStateOperEnabled;
    }
    elsif ( $descr =~ /Absent/ ) {
        $trap_oid = $entStateOperDisabled;
    }
    else {
        return;
    }

    my @varbinds;
    push @varbinds, $trap_oid;

    my $id = $einfo->{'Sensor Number'};
    return if ( !defined($id) );
    push @varbinds, "$entPhysicalIndex i ${\hex($id)}";

    my $sensor_name = $einfo->{'Sensor ID'};
    if ( defined($sensor_name) ) {
        my ( $name, undef ) = split / /, $sensor_name, 2;
        push @varbinds, "$entPhysicalDescr s '$name'";
    }

    push @varbinds, "$entStateAdmin i $EntityStateAdmin{'unlocked'}";
    push @varbinds, "$entStateAlarm b $EntityStateAlarm{'unknown'}";

    return join( ' ', @varbinds );
}

sub get_sensor_trap_varbinds {
    my $einfo = shift;
    return if ( !defined($einfo) );

    my $type = $einfo->{'Sensor Type'};
    return if ( !defined($type) );

    return
      if (
        !(
               $type eq "Temperature"
            || $type eq "Voltage"
            || $type eq "Fan"
            || $type eq "Current"
        )
      );

    my $descr = $einfo->{'Description'};
    return if ( !defined($descr) );
    my ( undef, $threshold_type ) = split / /, $descr, 3;
    my $trap_oid;
    if ( $threshold_type eq "Non-recoverable" ) {
        $trap_oid = $entSensorThresholdNonRecoverable;
    }
    elsif ( $threshold_type eq "Critical" ) {
        $trap_oid = $entSensorThresholdCritical;
    }
    elsif ( $threshold_type eq "Non-critical" ) {
        $trap_oid = $entSensorThresholdNonCritical;
    }
    else {
        return;
    }

    my @varbinds;
    push @varbinds, $trap_oid;

    my $id = $einfo->{'Sensor Number'};
    return if ( !defined($id) );
    push @varbinds, "$entPhysicalIndex i ${\hex($id)}";

    my $sensor_name = $einfo->{'Sensor ID'};
    if ( defined($sensor_name) ) {
        my ( $name, undef ) = split / /, $sensor_name, 2;
        push @varbinds, "$entPhysicalDescr s '$name'";
    }

    my $r = $einfo->{'Trigger Reading'};
    return if ( !defined($r) );
    my ($reading) = split /([a-zA-Z])/, $r, 2;
    $reading *= 10**$SensorPrecision{ $einfo->{'Sensor Type'} };
    push @varbinds, "$entSensorTriggerReading i $reading";

    my $t = $einfo->{'Trigger Threshold'};
    return if ( !defined($t) );
    my ($threshold) = split /([a-zA-Z])/, $t, 2;
    $threshold *= 10**$SensorPrecision{ $einfo->{'Sensor Type'} };
    push @varbinds, "$entSensorTriggerThreshold i $threshold";

    push @varbinds, "$entSensorTriggerDescription s '$descr'";

    return join( ' ', @varbinds );
}

sub is_sel_id {
    my ( $id, $size ) = @_;
    return 0 if ( !defined($id) || !defined($size) );
    return 1 if $id =~ /^[0-9a-fA-F]{$size}$/;
    return 0;
}

sub get_sel_entry_info {
    my ($id) = @_;
    return if ( !is_sel_id( $id, 4 ) );

    my %entry_info = ();
    my $cmd        = "ipmitool sel get 0x$id";
    if ( open( my $entry, "$cmd |" ) ) {
        while ( my $line = <$entry> ) {
            next if ( $line !~ /:/ );
            my ( $key, $value ) = split( ':', $line, 2 );
            next if ( !defined($key) || !defined($value) );
            $entry_info{ trim($key) } = trim($value);
        }
    }
    return \%entry_info;
}

sub get_state_trap_varbinds {
    my $einfo = shift;
    return if ( !defined($einfo) );

    my $type = $einfo->{'Sensor Type'};
    return if ( !defined($type) );
    my $descr = $einfo->{'Description'};
    return if ( !defined($descr) );
    return
      if ( !( $descr =~ /Absent/ || $descr =~ /Present/ ) );
    my @state_traps = ();

    return if ( exists $last_saved_state{$type} ) and ( $last_saved_state{$type} eq $descr );

    my $fru_vbinds = get_fru_varbinds( $einfo, $descr );
    push @state_traps, $fru_vbinds if ( defined($fru_vbinds) );
    my $oper_vbinds = get_state_oper_varbinds( $einfo, $descr );
    push @state_traps, $oper_vbinds if ( defined($oper_vbinds) );
    my $state_vbinds = get_status_change_varbinds( $einfo, $descr );
    push @state_traps, $state_vbinds if ( defined($state_vbinds) );
    my $config_vbinds = get_config_change_varbinds($einfo);
    push @state_traps, $config_vbinds if ( defined($config_vbinds) );
    $last_saved_state{$type} = $descr;

    return \@state_traps;
}

sub syslog_sel_message {
    my $einfo = shift;
    return if ( !defined($einfo) );

    my @msg   = ();
    my $stype = $einfo->{'Sensor Type'};
    my $id    = $einfo->{'Sensor ID'};
    if ( !defined($id) ) {
        $id = "";
        my $num = $einfo->{'Sensor Number'};
        $id = "(0x$num)" if ( defined($num) );
    }
    my $descr = $einfo->{'Description'};
    my $edir  = $einfo->{'Event Direction'};
    my $etype = $einfo->{'Event Type'};
    return
      if ( !defined($stype)
        || !defined($descr)
        || !defined($edir)
        || !defined($etype) );
    push @msg, $stype;
    push @msg, "sensor";
    push @msg, $id;
    push @msg, $descr;
    push @msg, "Asserted" if ( $edir =~ /Assertion/ );
    push @msg, "Deasserted" if ( $edir =~ /Deassertion/ );

    if ( $etype eq "Threshold" ) {
        my $rising = ( split / /, $descr )[-1];
        return
          if ( !defined($rising)
            || !( $rising eq "low" || $rising eq "high" ) );
        my $reading   = $einfo->{'Trigger Reading'};
        my $threshold = $einfo->{'Trigger Threshold'};
        return
          if ( !defined($rising)
            || !defined($reading)
            || !defined($threshold) );
        push @msg,
          "(Reading $reading $thresholdRising{$rising} Threshold $threshold)";
    }
    my $syslog_msg = join( ' ', @msg );
    syslog( 'info', $syslog_msg );
}

sub get_trap_varbinds {
    my ($id) = @_;

    return if ( !is_sel_id( $id, 4 ) );

    my $einfo = get_sel_entry_info($id);

    syslog_sel_message($einfo) if ($syslog_enabled);
    my @traps_to_send = ();
    if ($send_entity_state_traps) {
        my $state_vbinds = get_state_trap_varbinds($einfo);
        @traps_to_send = @$state_vbinds if ( defined($state_vbinds) );
    }
    if ($send_entity_sensor_traps) {
        my $sensor_vbinds = get_sensor_trap_varbinds($einfo);
        push @traps_to_send, $sensor_vbinds if ( defined($sensor_vbinds) );
    }
    return \@traps_to_send;
}

sub get_sel_count {
    my $cmd   = "ipmitool sel info";
    my $count = 0;
    my %sel_info;
    if ( open( my $entry, "$cmd |" ) ) {
        while ( my $line = <$entry> ) {
            next if ( $line !~ /:/ );
            my ( $key, $value ) = split( ':', $line, 2 );
            next if ( !defined($key) || !defined($value) );
            $sel_info{ trim($key) } = trim($value);
        }
        $count = $sel_info{'Entries'};
        if ( defined($selmgmt_mode) ) {
            my $free    = $sel_info{'# Free Units'};
            my $p       = $sel_info{'Percent Used'};
            my $percent = $p =~ s/%//;
            if ( $percent == $SEL_FULL || $free == 0 ) {
                syslog( 'alert', $SEL_FULL_ALERT )
                  if ( $selmgmt_mode eq 'capacity' );
            }
            syslog( 'warning', $SEL_THRESHOLD_WARNING )
              if ( $percent > $SEL_PERCENTUSED_THRESHOLD );
        }
    }
    return $count;
}

sub sel_entries_exist {
    return 1 if ( defined($selmgmt_mode) && $selmgmt_mode eq 'circular' );
    my $old_count = $sel_count;
    $sel_count = get_sel_count();

    return 0 if ( !defined($sel_count) );
    if ( $sel_count == 0 ) {
        $sel_last_id = "0000";
        return 0;
    }
    return 0 if ( $sel_count == $old_count );
    if ( $sel_count < $old_count ) {
        my $old_last_id = $sel_last_id;
        $sel_last_id = get_last_id();
        return 0 if ( $sel_last_id eq $old_last_id );
        $reset_last_id = 1;
    }
    return 1;
}

sub get_next_id {
    my ($id) = @_;

    return if ( !is_sel_id( $id, 4 ) );
    my ( $msid, $lsid ) = ( $id =~ /^(.{2})(.{2})/ );
    return if ( !is_sel_id( $msid, 2 ) || !is_sel_id( $lsid, 2 ) );
    my $resp = `$ipmi_get_sel_rec 0x$lsid 0x$msid $rec_offset_len 2> /dev/null`;
    return if ( !defined($resp) );
    my ( $next_lsid, $next_msid ) = split( ' ', $resp, 3 );
    return if ( !is_sel_id( $next_lsid, 2 ) || !is_sel_id( $next_msid, 2 ) );
    return "$next_msid$next_lsid";
}

sub get_last_id {
    my $resp = `$ipmi_get_sel_rec 0xff 0xff $rec_offset_len 2> /dev/null`;
    return if ( !defined($resp) );
    my ( undef, undef, $lsid, $msid ) = split( ' ', $resp, 5 );
    return if ( !is_sel_id( $lsid, 2 ) || !is_sel_id( $msid, 2 ) );
    return "$msid$lsid";
}

sub send_sensor_traps {
    $sel_last_id = get_last_id();
    openlog( 'selmgmt', 'ndelay,pid', 'daemon' );
    while (1) {
        my $start = time;
        if ( is_sel_id( $sel_last_id, 4 ) && sel_entries_exist() ) {
            my $curr_id = "0000";
            my $next_id = $sel_last_id;
            while ( is_sel_id( $next_id, 4 ) && $next_id ne $sel_end_id ) {
                $curr_id = $next_id;
                if (   $reset_last_id
                    || $curr_id ne $sel_last_id
                    || $curr_id eq "0000" )
                {
                    $reset_last_id = 0;
                    my $vbinds = get_trap_varbinds($curr_id);
                    foreach my $vbind (@$vbinds) {
                        system("$TRAP_CMD $vbind") if ( defined($vbind) );
                    }
                    $curr_id = get_last_id()
                      if ( $curr_id eq "0000" && $sel_last_id eq "0000" );
                }
                $next_id = get_next_id($next_id);
            }
            $sel_last_id = $curr_id;
            $sel_last_id = "0000" if ( !defined($next_id) );
        }
        my $remaining = $INTERVAL - ( time - $start );
        sleep($remaining) if ( $remaining > 0 );
        $sel_last_id = "0000" if ( !defined($sel_last_id) );
    }
    closelog();
}

sub has_ipmi {
    foreach my $idev (@IPMI_DEVICES) {
        return 1 if ( -e $idev );
    }
    return 0;
}

sub send_traps {
    return 1
      if (
        has_ipmi()
        && (   $send_entity_sensor_traps
            || $send_entity_state_traps
            || $syslog_enabled
            || defined($selmgmt_mode) )
      );
    return 0;
}

if ( open( $fh2, '>', '/tmp/entity-sensor-ipmi.log' ) ) {
    autoflush $fh2 1;
}

if ( open( my $fh, '<', $NOTIFICATIONS_ARGS_FILE ) ) {
    while ( my $line = <$fh> ) {
        chomp($line);
        $send_entity_sensor_traps = 1 if ( $line eq "sensor" );
        $send_entity_state_traps  = 1 if ( $line eq "state" );
    }
}

if ( open( my $fh, '<', $SELMGMT_ARGS_FILE ) ) {
    while ( my $line = <$fh> ) {
        chomp($line);
        $syslog_enabled = 1 if ( $line =~ "syslog" );
        $selmgmt_mode = $line if ( $line eq "capacity" || $line eq "circular" );
    }
    close($fh);
}

send_sensor_traps() if ( send_traps() );
