Create and Update OTRS Tickets from the Command-Line

I recently wrote a notification script for Centreon / Nagios to create and update tickets in OTRS. The ticket details and OTRS connection settings are all defined on the command-line. The GenericTicketConnector.yml must first be installed in OTRS, and a user (aka “Agent”) created for the script. I used perl’s taint mode, so had to hard-code the various log file locations ($logfile, $csvfile, and $dbfile). The Log::Handler module allows the script to output and log different amounts of activity detail, and the DBD::SQLite module is used to keep a local database of the Ticket ID (from OTRS) and the Problem ID (from Centreon / Nagios) associations — so the OTRS ticket can be updated with follow-up notifications from Centreon / Nagios for the same issue.

A few OTRS dynamic fields are used to improve OTRS searches, reports, etc. The ProblemID, HostName, HostAddress and ServiceDesc dynamic fields must be defined in OTRS before-hand. Although most command line arguments are mandatory, a few have default values if they are not specified. The script’s top-most comment section has a concise list of “Requirements for OTRS” and the Wiki Page on Google Code has additional installation notes for Centreon and OTRS.

#!/usr/bin/perl -Tw

# Copyright 2012 - Jean-Sebastien Morisset - https://surniaulula.com/
#
# Create and update OTRS tickets from Centreon, Nagios, other monitoring tools,
# or the command-line.
#
#   Blog Page: https://surniaulula.com/2012/10/24/create-and-update-otrs-tickets-from-the-command-line/
# Google Code: https://code.google.com/p/otrs-ticket/
#
# This script 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 3 of the License, or (at your option) any later
# version.
#
# This script 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 at http://www.gnu.org/licenses/.
#
# Centreon / Nagios Host Notification:
#
#	$USER1$/otrs-ticket.pl --otrs_user="user" --otrs_pass="pass" --otrs_server="server.domain.com:80" --problem_id="$HOSTPROBLEMID$" --problem_id_last="$LASTHOSTPROBLEMID$" --event_type="$NOTIFICATIONTYPE$" --event_date="$LONGDATETIME$" --event_host="$HOSTNAME$" --event_addr="$HOSTADDRESS$" --event_desc="$SERVICEACKAUTHOR$ $SERVICEACKCOMMENT$" --event_state="$HOSTSTATE$" --event_output="$HOSTOUTPUT$"
#
# Centreon / Nagios Service Notification:
#
#	 $USER1$/otrs-ticket.pl --otrs_user="user" --otrs_pass="pass" --otrs_server="server.domain.com:80" --problem_id="$SERVICEPROBLEMID$" --problem_id_last="$LASTSERVICEPROBLEMID$" --event_type="$NOTIFICATIONTYPE$" --event_date="$LONGDATETIME$" --event_host="$HOSTALIAS$" --event_addr="$HOSTADDRESS$" --event_desc="$SERVICEDESC$" --event_state="$SERVICESTATE$" --event_output="$SERVICEOUTPUT$"
#
# Requirements for OTRS:
#
# 1) The GenericTicketConnector.yml must be installed
#    (http://source.otrs.org/viewvc.cgi/otrs/development/webservices/GenericTicketConnector.yml?view=co)
# 1) A user name (aka "Agent") and password to for the script
# 2) A ticket queue (defaults to "UNIX" -- see the %otrs_defaults variable)
# 3) An 'unknown' customer username (see the %otrs_defaults variable)
# 5) An 'Infrastructure::Server::Unix/Linux' OTRS Service (see the
#    %otrs_defaults variable).
# 4) An OTRS State named 'recovered' (see the %otrs_states variable).
# 6) Dynamic fields ProblemID, HostName, HostAddress, and ServiceDesc.

# Changes:
#
# v1.2.1:
# - Modified the 'open()' function for the CSV file to use a proper variable name.
# - Passed the script through perlcritic to make sure all syntax is OK.
#
# v1.2
# - Renamed the event_id and event_id_last options to problem_id and problem_id_last.
# - Added %otrs_states to change ticket state depending on the event_type value.
# 
# v1.1
# - Added inet_aton/inet_ntoa function calls to resolve OTRS server IP before launching SOAP (just to make sure the resolver works).
# - Added --notif_id and --notif_number command line argument.
# - Renamed Nagios *EVENTID variables to *PROBLEMID.


use strict;
use Socket;
use Getopt::Long;
use DBI;
use DBD::SQLite;
use SOAP::Lite;
use Log::Handler;

my $VERSION = '1.2.1';

# hard-code paths to prevent warning from taint mode
my $logfile = '/var/tmp/otrs-ticket.log';
my $csvfile = '/var/tmp/otrs-ticket.csv';
my $dbname = '/var/tmp/otrs-ticket.sqlite';
my $dbuser = '';
my $dbpass = '';
my $dbtable = 'TicketIDAssoc';
# if the event_type is known, then change the ticket state
my %otrs_states = (
	'ACKNOWLEDGEMENT' => 'open',
	'RECOVERY' => 'recovered',
);
my %otrs_defaults = (
	'Queue' => 'UNIX',
	'PriorityID' => '3',
	'Type' => 'Incident',
	'State' => 'new',
	'CustomerUser' => 'unknown',
	'Service' => 'Infrastructure::Server::Unix/Linux',
);
my $TicketID;
my $TicketNumber;
my $ArticleID;

# read command line options
my %opt = ();
GetOptions(\%opt, 'verbose', 'otrs_user=s', 'otrs_pass=s', 'otrs_server=s',
'problem_id=s', 'problem_id_last=s', 'event_type=s', 'event_date=s',
'event_host=s', 'event_addr=s', 'event_desc=s', 'event_state=s',
'event_output=s', 'otrs_customer=s', 'otrs_queue=s', 'otrs_priority=s',
'otrs_type=s', 'otrs_state=s', 'otrs_service=s');

# silently strip anything non-numeric from integer fields (where-as
# using GetOptions's '=i' would throw an error)
for ( qw( problem_id problem_id_last ) ) { 
	$opt{$_} =~ s/[^0-9]// if (defined $opt{$_});
}
# clear "empty" event_desc from host notification
$opt{'event_desc'} =~ s/^\$ \$$//;

# beautify some option names for logging, ticket text, etc.
my %event_info = (
	'ProblemID' => $opt{'problem_id'} ||= 0,
	'ProblemIDLast' => $opt{'problem_id_last'} ||= 0,
	'EventType' => $opt{'event_type'} ||= '',
	'EventDate' => $opt{'event_date'} ||= '',
	'EventHostName' => $opt{'event_host'} ||= '',
	'EventHostAddress' => $opt{'event_addr'} ||= '',
	'EventServiceDesc' => $opt{'event_desc'} ||= '',
	'EventState' => $opt{'event_state'} ||= '',
	'EventOutput' => $opt{'event_output'} ||= '',
);

if (defined $opt{'problem_id'} && $opt{'problem_id'} == 0
	&& defined $opt{'problem_id_last'} && $opt{'problem_id_last'} > 0) {
	$opt{'problem_id'} = $opt{'problem_id_last'};
}

# define a new ticket state if one wasn't given on the command line, and the
# event_type has been defined in %otrs_states.
$opt{'otrs_state'} = $otrs_states{$opt{'event_type'}}
	if ( !$opt{'otrs_state'} 
		&& defined $otrs_states{$opt{'event_type'}}
		&& $otrs_states{$opt{'event_type'}} );

my $stdout = $opt{'verbose'} ? 'debug' : 'info';
my $log = Log::Handler->new();
$log->add(
	file => {
		filename => $logfile,
		maxlevel => 'debug',
		timeformat => '%Y%m%d-%H%M%S',
        },
	screen => {
		log_to   => 'STDOUT',
		maxlevel => $stdout,
		timeformat => '%Y%m%d-%H%M%S',
	},
);

$log->info("START of $0 v$VERSION script");

#
# Log the command line options to a csv file to keep a history (even if some
# arguments might be missing).
#
$log->debug("Saving event_info fields to $csvfile.");
unless (open (my $csv_fh, ">>", $csvfile)) { $log->critical("Error opening ".$csvfile.": ".$!); &DoExit(1); }
unless (-s $csvfile) { for (sort keys %event_info) { print $csv_fh '"', $_, '",'; }; print $csv_fh "\n"; }
for (sort keys %event_info) { print $csv_fh '"', $event_info{$_}, '",'; }; print $csv_fh "\n";
close ($csv_fh);

#
# Check all essential opt values and exit if some missing.
#
my @essential_opts = sort qw( otrs_user otrs_pass otrs_server problem_id event_type
event_date event_host event_addr event_state event_output );

# print the whole list before exiting
for (@essential_opts) {
	$log->error("Required argument $_ not defined or empty!") 
		if (!defined $opt{$_} || $opt{$_} eq '');
}
for (@essential_opts) { &DoExit(1) if (! $opt{$_}); }

for (sort keys %opt) {
	if ($_ eq 'otrs_pass' ) { $log->debug("Argument $_ = ********") }
	else { $log->debug("Argument $_ = $opt{$_}"); }
}

#
# Open the database and create the table(s) if necessary
#
my $dsn = "DBI:SQLite:dbname=$dbname";
my $dbh = DBI->connect($dsn, $dbuser, $dbpass);
if ($DBI::err) { $log->critical($DBI::errstr); &DoExit(1); }

$dbh->do("PRAGMA foreign_keys = ON");
$dbh->do("CREATE TABLE IF NOT EXISTS $dbtable ( 
	ProblemID INTEGER PRIMARY KEY, 
	TicketID INTEGER NOT NULL, 
	TicketNumber INTEGER )");
($TicketID, $TicketNumber) = $dbh->selectrow_array("SELECT TicketID, TicketNumber 
	FROM $dbtable WHERE ProblemID=?", undef, $opt{'problem_id'});

#
# Configuration for OTRS connection and definition of available Ticket /
# Article fields (used when constructing the SOAP data).
#
my %otrs = (
	'UserLogin' => $opt{'otrs_user'},
	'Password' => $opt{'otrs_pass'},
	'URL' => 'http://'.$opt{'otrs_server'}.'/otrs/nph-genericinterface.pl/Webservice/GenericTicketConnector',
	'NameSpace' => 'http://www.otrs.org/TicketConnector/',
	'TicketID' => '',
	'TicketNumber' => '',
	'Operation' => '',
	'TicketFields' => [
		'Title',
		'QueueID',
		'Queue',
		'TypeID',
		'Type',
		'ServiceID',
		'Service',
		'SLAID',
		'SLA',
		'StateID',
		'State',
		'PriorityID',
		'Priority',
		'OwnerID',
		'Owner',
		'ResponsibleID',
		'Responsible',
		'CustomerUser',
	],
	'ArticleFields' => [ 
		'ArticleTypeID',
		'ArticleType',
		'SenderTypeID',
		'SenderType',
		'Subject',
		'Body',
		'ContentType',
		'Charset',
		'MimeType',
		'HistoryType',
		'HistoryComment',
		'AutoResponseType',
		'TimeUnit',
		'NoAgentNotify',
		'ForceNotificationToUserID',
		'ExcludeNotificationToUserID',
		'ExcludeMuteNotificationToUserID',
	],
);


#
# Define the ticket details here.
#
my %ticket;
if ($TicketID) {
	$log->info("Found ProblemID $opt{'problem_id'} in database");
	$log->info("Updating TicketID $TicketID (TicketNumber $TicketNumber)");
	$otrs{'Operation'} = 'TicketUpdate';
	$otrs{'TicketID'} = $TicketID;
	# if we have a different state (than new) defined, then use it, otherwise leave as-is
	if (defined $opt{'otrs_state'} && $opt{'otrs_state'}) {
		$ticket{'State'} = $opt{'otrs_state'};
		$log->notice('Updating Ticket State to "'.$ticket{'State'}.'"');
	}
} else {
	$log->debug("ProblemID ".$opt{'problem_id'}." not found in database");
	$log->info("Creating new OTRS Ticket for ProblemID ".$opt{'problem_id'});
	$otrs{'Operation'} = 'TicketCreate';
	%ticket = (
		'Queue' => $opt{'otrs_queue'} ||= $otrs_defaults{'Queue'},
		'PriorityID' => $opt{'otrs_priority'} ||= $otrs_defaults{'PriorityID'},
		'Type' => $opt{'otrs_type'} ||= $otrs_defaults{'Type'},
		'State' => $opt{'otrs_state'} ||= $otrs_defaults{'State'},
		'Service' => $opt{'otrs_service'} ||= $otrs_defaults{'Service'},
		'DynamicField' => {
			'ProblemID' => $opt{'problem_id'},
			'HostName' => $opt{'event_host'},
			'HostAddress' => $opt{'event_addr'},
			'ServiceDesc' => $opt{'event_desc'},
		},
	);
}

# Common ticket fields / values for TicketUpdate or TicketCreate.
$ticket{'CustomerUser'} = $opt{'otrs_customer'} ||= $otrs_defaults{'CustomerUser'};
$ticket{'ContentType'} = 'text/plain; charset=utf8';
$ticket{'SenderType'} = 'system';
$ticket{'Title'} = $opt{'event_type'}.': '.$opt{'event_host'};
$ticket{'Title'} .= '/'.$opt{'event_desc'} if ($opt{'event_desc'});
$ticket{'Title'} .= ' is '.$opt{'event_state'};
$ticket{'Subject'} = $ticket{'Title'};
$ticket{'Body'} = $opt{'event_output'}."\n\n";

# Append all the "event_info" fields to the ticket for reference.
for (sort keys %event_info) { $ticket{'Body'} .= "$_ = $event_info{$_}\n"; }

#
# Convert Ticket and Article data into SOAP data structure
#
my @SOAPTicketData = ();
for my $el (@{$otrs{'TicketFields'}}) {
	if ( $ticket{$el}) {
		for (split (/\n/, $ticket{$el})) {
			$log->debug("TicketData $el = $_"); }
		push @SOAPTicketData, SOAP::Data->name($el => $ticket{$el});
	}
}

my @SOAPArticleData = ();
for my $el (@{$otrs{'ArticleFields'}}) {
	if ( $ticket{$el} ) {
		for (split (/\n/, $ticket{$el})) {
			$log->debug("ArticleData $el = $_"); }
		push @SOAPArticleData, SOAP::Data->name( $el => $ticket{$el} );
	}
}

# Dynamic Fields must be created in OTRS first.
my $DynamicFieldXML;
for ( sort keys %{$ticket{'DynamicField'}} ) {
	if ( $ticket{'DynamicField'}->{$_} ) {
		$log->debug("ArticleData $_ = $ticket{'DynamicField'}->{$_}");
		$DynamicFieldXML .= '<DynamicField><Name><![CDATA['.$_.']]></Name>'
			.'<Value><![CDATA['.$ticket{'DynamicField'}->{$_}.']]></Value></DynamicField>'."\n";
	}
}

if ($opt{'otrs_server'} =~ /^([^:]*)/) {
	my $ip_nbo = inet_aton($1);
	if (!$ip_nbo) { $log->critical("Failed to resolve IP of ".$1); &DoExit(1); }
	$log->info( "OTRS Server is ".$opt{'otrs_server'}." (".inet_ntoa($ip_nbo).")" );
}

my $soap_op = $otrs{'Operation'}; $log->info("SOAP $soap_op at ".$otrs{'URL'});
my $soap_obj = SOAP::Lite->uri($otrs{'NameSpace'})->proxy($otrs{'URL'})->$soap_op(
	SOAP::Data->name('UserLogin')->value($otrs{'UserLogin'}),
    	SOAP::Data->name('Password')->value($otrs{'Password'}),
    	SOAP::Data->name('TicketID')->value($otrs{'TicketID'}),
    	SOAP::Data->name('TicketNumber')->value($otrs{'TicketNumber'}),
	SOAP::Data->name('Ticket' => \SOAP::Data->value(@SOAPTicketData)),
	SOAP::Data->name('Article' => \SOAP::Data->value(@SOAPArticleData)),
	SOAP::Data->type('xml'=> $DynamicFieldXML),
);

if ( $soap_obj->fault ) { $log->critical($soap_obj->faultcode.": ".$soap_obj->faultstring); &DoExit(1); }

$log->info("SOAP transaction successful");

# get the XML response part from the SOAP message
my $XMLResponse = $soap_obj->context()->transport()->proxy()->http_response()->content();

# deserialize response (convert it into a perl structure)
my $Deserialized = eval { SOAP::Deserializer->deserialize($XMLResponse); };

# remove all the headers and other not needed parts of the SOAP message
my $Body = $Deserialized->body();

# check if ticket was created or updated
my $Response = $Body->{'TicketCreateResponse'} ? 
	'TicketCreateResponse' : 'TicketUpdateResponse';

if (defined $Body->{$Response}->{Error}) {
	$log->error("Error found in $Response");
	$log->error($Body->{$Response}->{Error}->{ErrorCode}." = ".$Body->{$Response}->{Error}->{ErrorMessage});
	&DoExit(1);
}

$TicketID = $Body->{$Response}->{TicketID};
$TicketNumber = $Body->{$Response}->{TicketNumber};
$ArticleID = $Body->{$Response}->{ArticleID};

my $ticket_sum = "TicketID $TicketID (TicketNumber $TicketNumber, ArticleID $ArticleID)";

if ($Response eq 'TicketUpdateResponse') { $log->info("Updated $ticket_sum"); }
else {
	$log->info("Created $ticket_sum");
	$log->info("Adding TicketID $TicketID and ProblemID $opt{'problem_id'} to $dbname");
	my $sth = $dbh->prepare("INSERT INTO $dbtable VALUES ( ?, ?, ? )");
	$sth->execute($opt{'problem_id'}, $TicketID, $TicketNumber);
}

&DoExit(0);

sub DoExit {
	my ($err) = @_;
	$log->info("END of $0 v$VERSION script");
	exit $err;
}

Download the otrs-ticket.pl script here or visit the
otrs-ticket project on Google Code for additional installation notes.

Here’s an example of the screen output when creating and updating a ticket.

$ ./otrs-ticket.pl --verbose \
        --otrs_user="centreon" \
        --otrs_pass="password" \
        --otrs_server="support.DOMAINNAME.com:80" \
        --otrs_customer="unknown" \
        --problem_id="1234" \
        --event_type="PROBLEM" \
        --event_date="Fri Oct 13 00:30:28 CDT 2000" \
        --event_host="TEST-TICKET-01" \
        --event_addr="127.0.0.1" \
        --event_desc="$ $" \
        --event_state="DOWN" \
        --event_output="Host Unreachable"

20121109-141217 [INFO] START of ./otrs-ticket.pl v1.2 script
20121109-141217 [DEBUG] Saving event_info fields to /var/tmp/otrs-ticket.csv.
20121109-141217 [DEBUG] Argument event_addr = 127.0.0.1
20121109-141217 [DEBUG] Argument event_date = Fri Oct 13 00:30:28 CDT 2000
20121109-141217 [DEBUG] Argument event_desc = 
20121109-141217 [DEBUG] Argument event_host = TEST-TICKET-01
20121109-141217 [DEBUG] Argument event_output = Host Unreachable
20121109-141217 [DEBUG] Argument event_state = DOWN
20121109-141217 [DEBUG] Argument event_type = PROBLEM
20121109-141217 [DEBUG] Argument otrs_customer = unknown
20121109-141217 [DEBUG] Argument otrs_pass = ********
20121109-141217 [DEBUG] Argument otrs_server = support.DOMAINNAME.com:80
20121109-141217 [DEBUG] Argument otrs_user = centreon
20121109-141217 [DEBUG] Argument problem_id = 1234
20121109-141217 [DEBUG] Argument problem_id_last = 0
20121109-141217 [DEBUG] Argument verbose = 1
20121109-141217 [DEBUG] ProblemID 1234 not found in database
20121109-141217 [INFO] Creating new OTRS Ticket for ProblemID 1234
20121109-141217 [DEBUG] TicketData Title = PROBLEM: TEST-TICKET-01 is DOWN
20121109-141217 [DEBUG] TicketData Queue = UNIX
20121109-141217 [DEBUG] TicketData Type = Incident
20121109-141217 [DEBUG] TicketData Service = Infrastructure::Server::Unix/Linux
20121109-141217 [DEBUG] TicketData State = new
20121109-141217 [DEBUG] TicketData PriorityID = 3
20121109-141217 [DEBUG] TicketData CustomerUser = unknown
20121109-141217 [DEBUG] ArticleData SenderType = system
20121109-141217 [DEBUG] ArticleData Subject = PROBLEM: TEST-TICKET-01 is DOWN
20121109-141217 [DEBUG] ArticleData Body = Host Unreachable
20121109-141217 [DEBUG] ArticleData Body = 
20121109-141217 [DEBUG] ArticleData Body = EventDate = Fri Oct 13 00:30:28 CDT 2000
20121109-141217 [DEBUG] ArticleData Body = EventHostAddress = 127.0.0.1
20121109-141217 [DEBUG] ArticleData Body = EventHostName = TEST-TICKET-01
20121109-141217 [DEBUG] ArticleData Body = EventOutput = Host Unreachable
20121109-141217 [DEBUG] ArticleData Body = EventServiceDesc = 
20121109-141217 [DEBUG] ArticleData Body = EventState = DOWN
20121109-141217 [DEBUG] ArticleData Body = EventType = PROBLEM
20121109-141217 [DEBUG] ArticleData Body = ProblemID = 1234
20121109-141217 [DEBUG] ArticleData Body = ProblemIDLast = 0
20121109-141217 [DEBUG] ArticleData ContentType = text/plain; charset=utf8
20121109-141217 [DEBUG] ArticleData HostAddress = 127.0.0.1
20121109-141217 [DEBUG] ArticleData HostName = TEST-TICKET-01
20121109-141217 [DEBUG] ArticleData ProblemID = 1234
20121109-141217 [INFO] OTRS Server is support.DOMAINNAME.com:80 (172.20.244.71)
20121109-141217 [INFO] SOAP TicketCreate at http://support.DOMAINNAME.com:80/otrs/nph-genericinterface.pl/Webservice/GenericTicketConnector
20121109-141217 [INFO] SOAP transaction successful
20121109-141217 [INFO] Created TicketID 954 (TicketNumber 2012110910000271, ArticleID 5174)
20121109-141217 [INFO] Adding TicketID 954 and ProblemID 1234 to /var/tmp/otrs-ticket.sqlite
20121109-141217 [INFO] END of ./otrs-ticket.pl v1.2 script
20121109-141248 [INFO] START of ./otrs-ticket.pl v1.2 script
20121109-141248 [INFO] Found ProblemID 1234 in database
20121109-141248 [INFO] Updating TicketID 954 (TicketNumber 2012110910000271)
20121109-141248 [NOTICE] Updating Ticket State to "recovered"
20121109-141248 [INFO] OTRS Server is support.DOMAINNAME.com:80 (172.20.244.71)
20121109-141248 [INFO] SOAP TicketUpdate at http://support.DOMAINNAME.com:80/otrs/nph-genericinterface.pl/Webservice/GenericTicketConnector
20121109-141248 [INFO] SOAP transaction successful
20121109-141248 [INFO] Updated TicketID 954 (TicketNumber 2012110910000271, ArticleID 5175)
20121109-141248 [INFO] END of ./otrs-ticket.pl v1.2 script
Find this content useful? Share it with your friends!