Group
Extension

App-RoboBot/lib/App/RoboBot/Plugin/API/PagerDuty.pm

package App::RoboBot::Plugin::API::PagerDuty;
$App::RoboBot::Plugin::API::PagerDuty::VERSION = '4.004';
use v5.20;

use namespace::autoclean;

use Moose;
use MooseX::SetOnce;

use AnyEvent;
use Data::Dumper;
use DateTime;
use HTTP::Request;
use JSON;
use LWP::UserAgent;
use LWP::Protocol::https;
use URI;

use App::RoboBot::Channel;
use App::RoboBot::Response;

extends 'App::RoboBot::Plugin';

=head1 api.pagerduty

Exports functions for interacting with PagerDuty API, and subscribing to alarm
notices.

API Keys for Pagerduty are currently part of the on-disk configuration file for
the bot, and as such there are no functions for adding/removing/changing oncall
groups or adding new Pagerduty accounts without restarting the bot. This will
likely change in a future release to make things easier for users to manage.

=cut

has '+name' => (
    default => 'API::PagerDuty',
);

has '+description' => (
    default => 'Exports functions for interacting with PagerDuty API, and subscribing to alarm notices.',
);

=head2 pagerduty-groups

=head3 Description

Displays the list of PagerDuty contact groups which currently have API keys
configured.

=head2 pagerduty-oncall

=head3 Description

Displays on-call information for the named group, based on the current schedule
in PagerDuty. All remaining arguments after the group name, if provided, will
be echoed back.

Calls to the on-call scheduling API at Pagerduty are cached briefly (for a few
minutes per oncall group) to prevent flooding their servers should anyone in
the channel call this function repeatedly.

=head3 Usage

<group> [<message>]

=head3 Examples

    :emphasize-lines: 2-5

    (pagerduty-oncall netops "I can't get a route from bastion to staging, help!")
    PagerDuty On-Call for Network Operations:
    Primary: Bobby Jo <bobby@nowhere.tld>
    Secondary: Janey Sue <janey@nowhere.tld>
    <Beauford> I can't get a route from bastion to staging, help!

=cut

has '+commands' => (
    default => sub {{
        'pagerduty-groups' => { method      => 'list_groups',
                                description => 'Displays the list of PagerDuty contact groups which currently have API keys configured.' },

        'pagerduty-oncall' => { method      => 'oncall',
                                description => 'Displays on-call information for the named group, based on the current schedule in PagerDuty. All remaining arguments after the group name, if provided, will be echoed back.',
                                usage       => '<group name> [<message>]', },
    }},
);

has 'ua' => (
    is      => 'ro',
    isa     => 'LWP::UserAgent',
    default => sub { LWP::UserAgent->new() },
);

has 'cache' => (
    is      => 'rw',
    isa     => 'HashRef',
    default => sub { { oncall => {} } },
);

sub init {
    my ($self, $bot) = @_;
}

sub list_groups {
    my ($self, $message, $command, $rpl) = @_;

    return unless $self->_validate_config($message);

    $message->response->push('The following PagerDuty groups are currently present in the configuration:');

    $message->response->push(
        sprintf('*%s*: %s',
            $_,
            ($self->bot->config->plugins->{'pagerduty'}{'group'}{$_}{'desc'} // 'no description'),
        ))
        for sort { lc($a) cmp lc($b) } keys %{$self->bot->config->plugins->{'pagerduty'}{'group'}};

    return;
}

sub oncall {
    my ($self, $message, $command, $rpl, $group, @extras) = @_;

    return unless $self->_validate_group_config($message, $group);

    my $now = $self->now;

    $group = $self->bot->config->plugins->{'pagerduty'}{'group'}{lc($group)};

    $message->response->push(sprintf('PagerDuty On-Call for _%s_:', $group->{'desc'}));

    foreach my $schedule (sort { $a->{'order'} <=> $b->{'order'} } @{$self->coerce_schedules($group->{'schedule'})}) {
        my $data;

        if (exists $self->cache->{'oncall'}{$group->{'name'}}{$schedule->{'id'}}
            && $self->cache->{'oncall'}{$group->{'name'}}{$schedule->{'id'}}{'expires'} > time())
        {
            $data = $self->cache->{'oncall'}{$group->{'name'}}{$schedule->{'id'}}{'data'};
        } else {
            $data = $self->make_pd_api_call($group, [qw( api v1 schedules ), $schedule->{'id'}, qw( entries )], { since => $now, until => $now });

            unless (defined $data) {
                $message->response->raise('An error was encountered contacting the PagerDuty API. Please try again.');
                return;
            }

            $self->cache->{'oncall'}{$group->{'name'}}{$schedule->{'id'}} = {
                data    => $data,
                expires => time() + 60,
            };
        }

        next unless exists $data->{'entries'} && ref($data->{'entries'}) eq 'ARRAY' && scalar(@{$data->{'entries'}}) > 0;
        my $entry = $data->{'entries'}[0];

        $message->response->push(sprintf('*%s*: %s <%s>',
            $schedule->{'name'},
            ($entry->{'user'}{'name'} // '_Nobody_'),
            ($entry->{'user'}{'email'} // '...'),
        ));
    }

    # TODO: Ugly hack for now to suppress defaulted string of "message" in the
    #       various !oncall<X> macros we use in Slack. Should be fixed long-term
    #       by revamping the macro variables code.
    if (@extras && @extras > 0 && lc($extras[0]) ne 'message') {
        $message->response->push(sprintf('<%s> %s', $message->sender->name, join(' ', @extras)));
    }

    return;
}

sub make_pd_api_call {
    my ($self, $group, $path, $args) = @_;

    my $uri = URI->new;
    $uri->scheme('https');
    $uri->host($group->{'domain'} . '.pagerduty.com');

    if (ref($path) eq 'ARRAY') {
        $uri->path_segments(@{$path});
    } else {
        $uri->path($path);
    }

    if (defined $args && ref($args) eq 'HASH' && scalar(keys(%{$args})) > 0) {
        $uri->query_form($args);
    }

    my $req = HTTP::Request->new( GET => $uri->as_string );
    $req->header( 'Content-type'  => 'application/json' );
    $req->header( 'Authorization' => sprintf('Token token=%s', $group->{'api_key'}) );

    my $response = $self->ua->request($req);

    return unless $response->is_success;

    my $json;
    eval {
        $json = decode_json($response->decoded_content);
    };
    return if $@;
    return $json;
}

sub now {
    my ($self) = @_;

    return DateTime->now->iso8601 . 'Z';
}

sub _validate_config {
    my ($self, $message) = @_;

    unless (exists $self->bot->config->plugins->{'pagerduty'}
            && exists $self->bot->config->plugins->{'pagerduty'}{'group'}
            && ref($self->bot->config->plugins->{'pagerduty'}{'group'}) eq 'HASH')
    {
        $message->response->raise('PagerDuty groups not properly configured. Please contact bot administrator.');
        return 0;
    }

    return 1;
}

sub _validate_group_config {
    my ($self, $message, $group) = @_;

    return 0 unless $self->_validate_config($message);

    my $lgroup = lc($group);

    unless (exists $self->bot->config->plugins->{'pagerduty'}{'group'}{$lgroup}
            && exists $self->bot->config->plugins->{'pagerduty'}{'group'}{$lgroup}{'api_key'}
            && exists $self->bot->config->plugins->{'pagerduty'}{'group'}{$lgroup}{'domain'})
    {
        $message->response->raise('The PagerDuty group %s is not properly configured. Please contact bot administrator.', $group);
        return 0;
    }

    return 1;
}

sub coerce_schedules {
    my ($self, $schedule) = @_;

    # If a group only has a single schedule defined, the config is going to
    # return it as a simple hashref with that schedule's data, but for simplicity
    # we want all the other functions to be able to assume it will be an arrayref
    # containing 0+ schedules.
    return $schedule if ref($schedule) eq 'ARRAY';
    return [$schedule] if ref($schedule) eq 'HASH';
    return [];
}

__PACKAGE__->meta->make_immutable;

1;


Powered by Groonga
Maintained by Kenichi Ishigaki <ishigaki@cpan.org>. If you find anything, submit it on GitHub.