Asterisk-AMI
view release on metacpan or search on metacpan
lib/Asterisk/AMI.pm view on Meta::CPAN
#!/usr/bin/perl
=head1 NAME
Asterisk::AMI - Perl module for interacting with the Asterisk Manager Interface
=head1 VERSION
0.2.8
=head1 SYNOPSIS
use Asterisk::AMI;
my $astman = Asterisk::AMI->new(PeerAddr => '127.0.0.1',
PeerPort => '5038',
Username => 'admin',
Secret => 'supersecret'
);
die "Unable to connect to asterisk" unless ($astman);
my $action = $astman->({ Action => 'Command',
Command => 'sip show peers'
});
=head1 DESCRIPTION
This module provides an interface to the Asterisk Manager Interface. It's goal is to provide a flexible, powerful, and
reliable way to interact with Asterisk upon which other applications may be built. It utilizes AnyEvent and therefore
can integrate very easily into event-based applications, but it still provides blocking functions for us with standard
scripting.
=head2 SSL SUPPORT INFORMATION
For SSL support you will also need the module that AnyEvent::Handle uses for SSL support, which is not a required
dependency. Currently that module is 'Net::SSLeay' (AnyEvent:Handle version 5.251) but it may change in the future.
=head3 CentOS/Redhat
If the version of Net:SSLeay included in CentOS/Redhat does not work try installing an updated version from CPAN.
=head2 Constructor
=head3 new([ARGS])
Creates a new AMI object which takes the arguments as key-value pairs.
Key-Value Pairs accepted:
PeerAddr Remote host address <hostname>
PeerPort Remote host port <service>
Events Enable/Disable Events 'on'|'off'
Username Username to access the AMI
Secret Secret used to connect to AMI
AuthType Authentication type to use for login 'plaintext'|'MD5'
UseSSL Enables/Disables SSL for the connection 0|1
BufferSize Maximum size of buffer, in number of actions
Timeout Default timeout of all actions in seconds
Handlers Hash reference of Handlers for events { 'EVENT' => \&somesub };
Keepalive Interval (in seconds) to periodically send 'Ping' actions to asterisk
TCP_Keepalive Enables/Disables SO_KEEPALIVE option on the socket 0|1
Blocking Enable/Disable blocking connects 0|1
on_connect A subroutine to run after we connect
on_connect_err A subroutine to call if we have an error while connecting
on_error A subroutine to call when an error occurs on the socket
on_disconnect A subroutine to call when the remote end disconnects
on_timeout A subroutine to call if our Keepalive times out
OriginateHack Changes settings to allow Async Originates to work 0|1
'PeerAddr' defaults to 127.0.0.1.
'PeerPort' defaults to 5038.
'Events' default is 'off'. May be anything that the AMI will accept as a part of the 'Events' parameter for the
login action.
'Username' has no default and must be supplied.
'Secret' has no default and must be supplied.
'AuthType' sets the authentication type to use for login. Default is 'plaintext'. Use 'MD5' for MD5 challenge
authentication.
'UseSSL' defaults to 0 (no ssl). When SSL is enabled the default PeerPort changes to 5039.
'BufferSize' has a default of 30000. It also acts as our max actionid before we reset the counter.
'Timeout' has a default of 0, which means no timeout on blocking.
'Handlers' accepts a hash reference setting a callback handler for the specified event. EVENT should match
the contents of the {'Event'} key of the event object will be. The handler should be a subroutine reference that
will be passed the a copy of the AMI object and the event object. The 'default' keyword can be used to set
a default event handler. If handlers are installed we do not buffer events and instead immediately dispatch them.
If no handler is specified for an event type and a 'default' was not set the event is discarded.
'Keepalive' only works when running with an event loop. Used with on_timeout, this can be used to detect if
asterisk has become un-responsive.
'TCP_Keepalive' default is disabled. Activates the tcp keep-alive at the socket layer. This does not require
an event-loop and is lightweight. Useful for applications that use long-lived connections to Asterisk but
do not run an event loop.
'Blocking' has a default of 1 (block on connecting). A value of 0 will cause us to queue our connection
and login for when an event loop is started. If set to non blocking we will always return a valid object.
'on_connect' is a subroutine to call when we have successfully connected and logged into the asterisk manager.
it will be passed our AMI object.
'on_connect_err', 'on_error', 'on_disconnect'
These three specify subroutines to call when errors occur. 'on_connect_err' is specifically for errors that
occur while connecting, as well as failed logins. If 'on_connect_err' or 'on_disconnect' it is not set,
but 'on_error' is, 'on_error' will be called. 'on_disconnect' is not reliable, as disconnects seem to get lumped
under 'on_error' instead. When the subroutine specified for any of theses is called the first argument is a copy
of our AMI object, and the second is a string containing a message/reason. All three of these are 'fatal', when
they occur we destroy our buffers and our socket connections.
'on_timeout' is called when a keep-alive has timed out, not when a normal action has. It is non-'fatal'.
The subroutine will be called with a copy of our AMI object and a message.
'OriginateHack' defaults to 0 (off). This essentially enables 'call' events and says 'discard all events
unless the user has explicitly enabled events' (prevents a memory leak). It does its best not to mess up
anything you have already set. Without this, if you use 'Async' with an 'Originate' the action will timeout
or never callback. You don't need this if you are already doing work with events, simply add 'call' events
to your eventmask.
=head2 Disabling Warnings
If you have warnings enabled this module will emit a number of them on connection errors, deprecated features, etc.
To disable this but still have all other warnings in perl enabled you can do the following:
use Asterisk::AMI;
use warnings;
no warnings qw(Asterisk::AMI);
That will enable warnings but disable any warnings from this module.
=head2 Warning - Mixing Event-loops and blocking actions
For an intro to Event-Based programming please check out the documentation in AnyEvent::Intro.
If you are running an event loop and use blocking methods (e.g. get_response, check_response, action,
simple_action, connected, or a blocking connect) the outcome is unspecified. It may work, it may lock everything up, the action may
work but break something else. I have tested it and behavior seems unpredictable at best and is very
circumstantial.
If you are running an event-loop use non-blocking callbacks! It is why they are there!
However if you do play with blocking methods inside of your loops let me know how it goes.
=head2 Actions
=head3 ActionIDs
lib/Asterisk/AMI.pm view on Meta::CPAN
And the contents of response will look similiar to the following:
{
'Message' => 'Originate successfully queued',
'EVENTS' => [
{
'Exten' => '100',
'CallerID' => '<unknown>',
'Event' => 'OriginateResponse',
'Privilege' => 'call,all',
'Channel' => 'SIP/peer-009c5510',
'Context' => 'some_context',
'Response' => 'Success',
'Reason' => '4',
'CallerIDName' => '<unknown>',
'Uniqueid' => '1276543236.82',
'ActionID' => '3',
'CallerIDNum' => '<unknown>'
}
],
'ActionID' => '3',
'GOOD' => 1,
'COMPLETED' => 1,
'Response' => 'Success'
};
More Info:
Check out the voip-info.org page for more information on the Originate action.
http://www.voip-info.org/wiki/view/Asterisk+Manager+API+Action+Originate
=head3 Callbacks
You may also specify a subroutine to callback when using send_action as well as a timeout.
An example of this would be:
$astman->send_action({ Action => 'Ping' }, \&somemethod, 7, $somevar);
In this example once the action 'Ping' finishes we will call somemethod() and pass it the a copy of our AMI object,
the Response Object for the action, and an optional variable $somevar. If a timeout is not specified
it will use the default set. A value of 0 means no timeout. When the timeout is reached somemethod() will be called
and passed a reference to our $astman and the uncompleted Response Object, therefore somemethod() should check the
state of the object. Checking the key {'GOOD'} is usually a good indication if the response is useable.
Anonymous subroutines are also acceptable as demostrated in the examples below:
my $callback = sub { return };
$astman->send_action({ Action => 'Ping' }, $callback, 7);
Or
$astman->send_action({ Action => 'Ping' }, sub { return }, 7);
=head3 Callback Caveats
Callbacks only work if we are processing packets, therefore you must be running an event loop. Alternatively, we run
mini-event loops for our blocking calls (e.g. action(), get_action()), so in theory if you set callbacks and then
issue a blocking call those callbacks should also get triggered. However this is an unsupported scenario.
Timeouts are done using timers and they are set as soon as you send the object. Therefore if you send an action with a
timeout and then monkey around for a long time before getting back to your event loop (to process input) you can time
out before ever even attempting to receive the response.
A very contrived example:
$astman->send_action({ Action => 'Ping' }, \&somemethod, 3);
sleep(4);
#Start loop
$astman->loop;
#Oh no we never even tried to get the response yet it will still time out
=head2 Passing Variables in an Action Response
Sometimes, when working in an event framework, you want a way to associate/map the response to an action with another
identifier used in your application. Normally you would have to maintain some sort of separate mapping involving the
ActionID to accomplish this. This modules provides a generic way to pass any perl scalar (this includes references)
with your action which is then passed to the callback with the response.
=head3 Passing
The variable to be passed to the callback should be passed as the fourth argument to the send_action() method.
For example to pass a simple scalar value:
my $vartostore = "Stored";
$astman->send_action({ Action => 'Ping' }, \&somemethod, undef, $vartostore });
And to pass a reference:
my @vartostore = ("One", "Two");
$astman->send_action({ Action => 'Ping' }, \&somemethod, undef, \@vartostore });
=head3 Retrieving
The passed variable will be available as the third argument to the callback.
To retrieve in a callback:
my ($astman, $resp, $store) = @_;
print $store . " was stored\n";
=head2 Responses and Events
NOTE: Empty fields sent by Asterisk (e.g. 'Account: ' with no value in an event) are represented by the hash
value of null string, not undef. This means you need to test for ''
(e.g. if ($response->{'Account'} ne '')) ) for any values that might be possibly be empty.
=head3 Responses
Responses are returned as response objects, which are hash references, structured as follows:
$response->{'Response'} Response to our packet (Success, Failed, Error, Pong, etc).
{'ActionID'} ActionID of this Response.
{'Message'} Message line of the response.
{'EVENTS'} Array reference containing Event Objects associated with this actionid.
{'PARSED'} Hash reference of lines we could parse into key->value pairs.
lib/Asterisk/AMI.pm view on Meta::CPAN
#and waits for the response before returning it.
sub _wait_response {
my ($self, $id, $timeout) = @_;
#Already got it?
if ($self->{RESPONSEBUFFER}->{$id}->{'COMPLETED'}) {
my $resp = $self->{RESPONSEBUFFER}->{$id};
delete $self->{RESPONSEBUFFER}->{$id};
delete $self->{CALLBACKS}->{$id};
delete $self->{EXPECTED}->{$id};
return $resp;
}
#Don't Have it, wait for it Install some handlers and use a CV to simulate blocking
my $process = AE::cv;
$self->{CALLBACKS}->{$id}->{'cb'} = sub { $process->send($_[1]) };
$timeout = $self->{CONFIG}->{TIMEOUT} unless (defined $timeout);
#Should not need to weaken here because this is a blocking call Only outcomes can be error, timeout, or
#complete, all of which will finish the cb and clear the reference weaken($self)
if ($timeout) {
$self->{CALLBACKS}->{$id}->{'timeout'} = sub {
my $response = $self->{'RESPONSEBUFFER'}->{$id};
delete $self->{RESPONSEBUFFER}->{$id};
delete $self->{CALLBACKS}->{$id};
delete $self->{EXPECTED}->{$id};
$process->send($response);
};
#Make sure event loop is up to date in case of sleeps
AE::now_update;
$self->{CALLBACKS}->{$id}->{'timer'} = AE::timer $timeout, 0, $self->{CALLBACKS}->{$id}->{'timeout'};
}
return $process->recv;
}
sub _build_action {
my ($actionhash, $id) = @_;
my $action;
my $async;
my $callback;
my $timeout;
#Create an action out of a hash
while (my ($key, $value) = each(%{$actionhash})) {
my $lkey = lc($key);
#Callbacks
if ($key eq 'CALLBACK') {
carp "Use of the CALLBACK key in an action is deprecated and will be removed in a future release.\n",
"Please use the syntax that is available." if warnings::enabled('Asterisk::AMI');
$callback = $actionhash->{$key} unless (defined $callback);
next;
#Timeout
} elsif ($key eq 'TIMEOUT') {
carp "Use of the TIMEOUT key in an action is deprecated and will be removed in a future release\n",
"Please use the syntax that is available." if warnings::enabled('Asterisk::AMI');
$timeout = $actionhash->{$key} unless (defined $timeout);
next;
#Exception of Orignate Async
} elsif ($lkey eq 'async' && $value == 1) {
$async = 1;
#Clean out user ActionIDs
} elsif ($lkey eq 'actionid') {
carp "User supplied ActionID being ignored." if warnings::enabled('Asterisk::AMI');
next;
}
#Handle multiple values
if (ref($value) eq 'ARRAY') {
foreach my $var (@{$value}) {
$action .= $key . ': ' . $var . "\015\012";
}
} else {
$action .= $key . ': ' . $value . "\015\012";
}
}
#Append ActionID and End Command
$action .= 'ActionID: ' . $id . "\015\012\015\012";
return ($action, $async, $callback, $timeout);
}
#Sends an action to the AMI Accepts an Array Returns the actionid of the action
sub send_action {
my ($self, $actionhash, $callback, $timeout, $store) = @_;
#No connection
return unless ($self->{handle});
#resets id number
if ($self->{idseq} > $self->{CONFIG}->{BUFFERSIZE}) {
$self->{idseq} = 1;
}
my $id = $self->{idseq}++;
#Store the Action ID
$self->{lastid} = $id;
#Delete anything that might be in the buffer
delete $self->{RESPONSEBUFFER}->{$id};
delete $self->{CALLBACKS}->{$id};
my ($action, $hcb, $htimeout);
($action, $self->{RESPONSEBUFFER}->{$id}->{'ASYNC'}, $hcb, $htimeout) = _build_action($actionhash, $id);
$callback = $hcb unless (defined $callback);
$timeout = $htimeout unless (defined $timeout);
if ($self->{LOGGEDIN} || lc($actionhash->{'Action'}) =~ /login|challenge/x) {
lib/Asterisk/AMI.pm view on Meta::CPAN
#Wait for the action to complete
my $resp = $self->_wait_response($actionid, $timeout);
if ($resp->{'COMPLETED'}) {
return $resp;
}
return;
}
#Sends an action and returns its data or undef if the command failed
sub action {
my ($self, $action, $timeout) = @_;
#Send action
my $actionid = $self->send_action($action);
if (defined $actionid) {
#Get response
return $self->get_response($actionid, $timeout);
}
return;
}
#Sends an action and returns 1 if it was successful and 0 if it failed
sub simple_action {
my ($self, $action, $timeout) = @_;
#Send action
my $actionid = $self->send_action($action);
if (defined $actionid) {
my $resp = $self->_wait_response($actionid, $timeout);
if ($resp->{'COMPLETED'}) {
return $resp->{'GOOD'};
}
}
return;
}
#Calculate md5 response to channel
sub _md5_resp {
my ($self, $challenge) = @_;
my $md5 = Digest::MD5->new();
$md5->add($challenge);
$md5->add($self->{CONFIG}->{SECRET});
return $md5->hexdigest;
}
#Logs into the AMI
sub _login {
my ($self) = @_;
#Auth challenge
my %challenge;
#Timeout to use
my $timeout;
$timeout = 5 unless ($self->{CONFIG}->{TIMEOUT});
#Build login action
my %action = ( Action => 'login',
Username => $self->{CONFIG}->{USERNAME},
Events => $self->{CONFIG}->{EVENTS} );
#Actions to take for different authtypes
if (lc($self->{CONFIG}->{AUTHTYPE}) eq 'md5') {
#Do a challenge
%challenge = ( Action => 'Challenge',
AuthType => $self->{CONFIG}->{AUTHTYPE});
} else {
$action{'Secret'} = $self->{CONFIG}->{SECRET};
}
#Blocking connect
if ($self->{CONFIG}->{BLOCKING}) {
return $self->_login_block(\%action, \%challenge, $timeout);
} else {
return $self->_login_noblock(\%action, \%challenge, $timeout);
}
return;
}
#Checks loging responses, prints errors
sub _logged_in {
my ($self, $login) = @_;
if ($login->{'GOOD'}) {
#Login was good
$self->{LOGGEDIN} = 1;
$self->{CONFIG}->{ON_CONNECT}->($self) if ($self->{CONFIG}->{ON_CONNECT});
#Flush pre-login buffer
foreach (values %{$self->{PRELOGIN}}) {
$self->{handle}->push_write($_);
}
delete $self->{PRELOGIN};
return 1;
} else {
#Login failed
if ($login->{'COMPLETED'}) {
$self->_on_connect_err("Login Failed to Asterisk (bad auth) at $self->{CONFIG}->{PEERADDR}:$self->{CONFIG}->{PEERPORT}");
} else {
$self->_on_connect_err("Login Failed to Asterisk due to timeout at $self->{CONFIG}->{PEERADDR}:$self->{CONFIG}->{PEERPORT}");
}
return;
}
return;
}
#Blocking Login
( run in 1.794 second using v1.01-cache-2.11-cpan-5a3173703d6 )