#!/usr/bin/perl -w

use 5.006;
use strict;
use warnings;

use Fcntl qw(LOCK_EX O_APPEND O_CREAT O_WRONLY);
use Getopt::Long 'GetOptions';
use IO::Socket::INET '$CRLF';
use noris::NetSaint;

use constant DEFAULT_INTERFACES => qw(krempel.backup.noris.net:54444);

my @argv = @ARGV;

my $Extern;
GetOptions(
    'expire=i' => \( my $Expire = 86400 ),
    'extern!' => \$Extern,
    'help|?' => sub {
        exec perldoc => -F => $0
          or die "exec('perldoc -F $0') returned $?: $!\n";
    },
    'info=s'      => \my $Info,
    'interface=s' => \my @Interfaces,
    'intern'      => sub { $Extern = '' },
    'key=s'       => \my $Key,
    'kunde=s'     => \my $Kunde,
    'logfile=s'   => \my $Logfile,
    'ok-states=s' => \( my $OK_States = 'OK UP' ),
    'priority=i'  => \my $Priority,
    'status=s'    => \my $Status,
    'status-db=s' => \my $StatusDB,
    'target=s'    => \my $Target,
    'timeout=f'   => \( my $Timeout = 3 ),
    'type=s'      => \my $Type,
  )
  or exit 1;

my $NetSaint = noris::NetSaint->new;
die "A -target must be given.\n" unless defined $Target;

die qq(Wrong usage; please call "$0 --help" for details.\n) if @ARGV;

my $log;
if ( defined $Logfile ) {
    unless ( sysopen $log, $Logfile, O_APPEND | O_CREAT | O_WRONLY ) {
        warn "sysopen('$log', O_APPEND | O_CREAT | O_WRONLY): $!\n";
    }
    elsif ( not flock $log, LOCK_EX ) {
        warn "flock('$log', LOCK_EX): $!\n";
    }
    else {
        print $log localtime() . " @argv";
    }
}

@Interfaces = DEFAULT_INTERFACES unless @Interfaces;

my %OK_State;
@OK_State{ split ' ', $OK_States } = ();

my %status;

if ( defined $Key && $Target =~ /\D/ ) {
    die "-key only works in conjunction with -status-db.\n"
      unless defined $StatusDB;
    dbmopen %status, $StatusDB, 0600 or die "dbmopen('$StatusDB',0600): $!\n";
    my ( $timestamp, $ticket ) = split ' ', $status{$Key}
      if defined $status{$Key};
    $Target = $ticket
      if $timestamp && $ticket
      and !$Expire || $timestamp + $Expire >= $^T;
}

my ( $interface, $socket );
for (@Interfaces) {
    $socket = IO::Socket::INET->new(
        PeerAddr => ( $interface = $_ ),
        $Timeout ? ( Timeout => $Timeout ) : ()
      )
      and last;

    # Fehlermeldung lautet bei lteren IO::Socket-Versionen
    # "IO::Socket::INET: Timeout", bei neueren
    # "IO::Socket::INET: connect: timeout"
    warn "Error connect()ing to $_: $@" unless $@ =~ /: Timeout$/i;
}

die "Could not connect to any interface.\n" unless $socket;
$socket->print("ticket_correspondence monitoring $Target\n")
  or warn "Error print()ing to $interface: $!\n";
$socket->print( 'X-noris-Ticket-'
      . ( $Target =~ m/\d{1,12}+/ ? 'Number' : 'Queue' )
      . ": $Target\n" )
  or warn "Error print()ing to $interface: $!\n";
if ( defined $Extern ) {
    $socket->print( 'X-noris-Ticket-ArticleType: '
          . ( $Extern ? 'extern' : 'intern' )
          . "\n" )
      or warn "Error print()ing to $interface: $!\n";
}
if ( defined $Info ) {
    $Info =~ s/\n+/ /g;
    $socket->print("X-noris-Ticket-Info: $Info\n")
      or warn "Error print()ing to $interface: $!\n";
}
if ( defined $Kunde ) {
    $Kunde =~ s/\s+//g;
    $socket->print("X-noris-Ticket-Kunde: $Kunde\n")
      or warn "Error print()ing to $interface: $!\n";
}
if ( defined $Priority ) {
    $socket->print("X-noris-Ticket-Priority: $Priority\n")
      or warn "Error print()ing to $interface: $!\n";
}
if ( defined $Type ) {
    $socket->print("X-noris-Ticket-Type: $Type\n")
      or warn "Error print()ing to $interface: $!\n";
}
while (<STDIN>) {
    $_ .= '.' if /^\.+$/;
    $socket->print($_) or warn "Error print()ing to $interface: $!\n";
}
$socket->print(".$CRLF");
my $ticket_seq;
{
    defined( $ticket_seq = <$socket> ) or last;

    # Hinweis auf Zustellung an hnliches Ticket ignorieren:
    redo if $ticket_seq =~ /^\(#\d+\)\n/;
}
$socket->close or die "Error closing connection to $interface: $!\n";

unless ( defined $ticket_seq ) {
    $NetSaint->update( Critical => "Das Ticketsystem lieferte keine Ticket-ID.\n" );
}
elsif ( $ticket_seq !~ /^(\d+-\d+)\s*\z/ ) {
    $NetSaint->update( Critical => "Das Ticketsystem sagt: $ticket_seq\n" );
}
else {
    $NetSaint->update( OK => "Der Artikel landete in Ticket $1." );
    if ( defined $Key ) {
        if ( defined $Status && exists $OK_State{$Status} ) {
            delete $status{$Key};
        }
        else { $ticket_seq =~ /^(\d+)/ and $status{$Key} = "$^T $1" }
    }
}

if ($log) {
    if ( defined $ticket_seq ) {
        $ticket_seq =~ /(.*)/;
        print $log " => Ticket #$1";
    }
    else {
        print $log " ! Fehler\n";
    }
}

END { print $log "\n" if $log }

=head1 NAME

notify_ticket - Alarme ans Ticketsystem senden

=head1 SYNOPSE

 cat Alarmtext |
 notify_ticket -key "$HOSTALIAS$/$SERVICEDESC$" \
               -status "$SERVICESTATE$"         \
               -target hotline

=head1 BESCHREIBUNG

Die Meldung selbst wird in Form einer Mail (inkl. Subject:-, jedoch ohne
To:-Header) von STDIN gelesen.

Man knnte Alarme auch ans Ticketsystem mailen, aber die Verwendung dieses
Scripts bringt mehrere Vorteile:

=over 4

=item *

Es sagt einem, in welchem Ticket die Nachricht gelandet ist
und welche Sequence-Nummer sie hat (und zwar auf der Standardausgabe).

=item *

Bei Angabe eines eineindeutigen L<Schlssels|/-key ID> fr den Dienst werden
Folgealarme (bis zum nchsten OK-Alert fr denselben Dienst) ins selbe Ticket
geschickt.
(Ausnahme: Wenn eine L<Expire-Zeit|/-expire Sekunden> angegeben und der letzte
Alarm lter als diese Zeitspanne ist, wird ein neues Ticket begonnen.)

=back

=head1 NOTWENDIGE ARGUMENTE

=over 4

=item -target Queue

=item -target Ticket-Nummer

Nummer des Tickets bzw. Name der Queue, in dem bzw. der der Alarm landen soll

=back

=head1 NOTWENDIGE ARGUMENTE

=over 4

=item -target Queue

=item -target Ticket-Nummer

Nummer des Tickets bzw. Name der RT-Queue, in dem bzw. der der Alarm landen soll

=back

=head1 OPTIONEN

Alle Optionen werden mittels
L<GetOptions() aus Getopt::Long|Getopt::Long/GetOptions> ausgewertet.

=over 4

=item -expire Sekunden

Zeit, nach der ein Eintrag der L<Status-Datenbank|/-status-db Datei>
ungltig werden soll

=item -extern

=item -intern

um explizit festzulegen, ob der Artikel fr Kunden sichtbar sein soll.
Wenn man die Option nicht verwendet, greift die allgemeine Magie, vgl.
Ticket 207789.

=item -help

=item -?

um (nur) diese Dokumentation anzeigen zu lassen

=item -info Text

Info-Text, der im Ticket gesetzt werden soll

=item -interface Host:Port

Host(s) und Port(s) der Socket-Schnittstelle(n) zum Ticketsystem;
Default: C<krempel.backup.office.noris.de:54444>

Kann mehrfach verwendet werden, um mehrere Schnittstellen anzugeben, die dann
der Reihe nach durchprobiert werden, bis es irgendwo klappt.

=item -key ID

Schlssel zur eineindeutigen Zuordnung des Alarms zu einem Service

Damit das funktioniert, muss ein Name fr die L<Status-DB|/-status-db Datei>
angegeben werden!

=item -kunde Kundenmerkmal

Kunde, den das geffnete Ticket bekommen
soll(, sofern ein neues geffnet wird)

=item -logfile Datei

Name einer Log-Datei, in der dann festgehalten wird, welche Ticket-Sequence
durch welchen Script-Aufruf entstanden ist, vgl. Ticket 221924

=item -ok-states Liste

Whitespace-getrennte Liste von Status, nach denen der Status gelscht werden
soll, so dass fr den nchsten Alarm dann ein neues Ticket begonnen wird.
Default: "OK UP"

=item -priority Prioritt

Prioritt, den das geffnete Ticket bekommen
soll(, sofern ein neues geffnet wird)

=item -status Status

aka Nagios-SERVICESTATE

=item -status-db Datei

Name der Datenbank-Datei, in der gespeichert wird, welcher Key zu welchem Ticket
gehrt

=item -timeout Sekunden(bruchteile)

Timeout fr Socket-Verbindungen in Sekunden

=item -type Tickettyp

Typt, den das geffnete Ticket bekommen soll(, sofern ein neues geffnet wird)

=back

=head1 AUTOR

 Stelios Gikas <stelios.gikas@noris.net>
 fr die noris network AG, Ticket 10040212

=cut

