Created
November 2, 2012 08:28
-
-
Save baskarp/3999485 to your computer and use it in GitHub Desktop.
Ruby script to mirror an email incident as an API incident in PagerDuty
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env ruby | |
# Ruby script to mirror an email incident as an API incident | |
# | |
# Copyright (c) 2011, PagerDuty, Inc. <[email protected]> | |
# All rights reserved. | |
# | |
# Redistribution and use in source and binary forms, with or without | |
# modification, are permitted provided that the following conditions are met: | |
# * Redistributions of source code must retain the above copyright | |
# notice, this list of conditions and the following disclaimer. | |
# * Redistributions in binary form must reproduce the above copyright | |
# notice, this list of conditions and the following disclaimer in the | |
# documentation and/or other materials provided with the distribution. | |
# * Neither the name of PagerDuty Inc nor the | |
# names of its contributors may be used to endorse or promote products | |
# derived from this software without specific prior written permission. | |
# | |
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND | |
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED | |
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE | |
# DISCLAIMED. IN NO EVENT SHALL PAGERDUTY INC BE LIABLE FOR ANY | |
# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES | |
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; | |
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND | |
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS | |
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
# | |
# Tested with Ruby 1.9.3 | |
# Requires rubygems (Ruby 1.9.3 comes with Rubygems) | |
# Just run the following from a terminal to install the necessary gems | |
# | |
# sudo gem install json | |
# sudo gem install faraday | |
# sudo gem install mail | |
# sudo gem install nokogiri | |
# | |
# Once this script is installed (i.e. in ~/user/mirror_incident.rb) | |
# Then edit the user's ctron tab to run this script every minute. | |
# | |
# crontab -u <user-name> -e | |
# | |
# Add the following line to the crontab: | |
# * * * * * ~/user/mirror_incident.rb | |
# | |
require 'rubygems' | |
require 'faraday' | |
require 'json' | |
require 'mail' | |
require 'nokogiri' | |
EVENTS_URL = "https://events.pagerduty.com" | |
EVENTS_PATH = "/generic/2010-04-15/create_event.json" | |
# Unfortunately HTTP Basic Authentication is not supported by PagerDuty, thus | |
# we need to use cookie based authentication | |
class PagerDutyAgent | |
attr_reader :email | |
attr_reader :connection | |
attr_reader :cookies | |
def initialize(subdomain, email, password) | |
@connection = Faraday.new(:url => "https://#{subdomain}.pagerduty.com", | |
:ssl => {:verify => false}) do |c| | |
c.request :url_encoded | |
# c.response :logger | |
c.adapter :net_http | |
end | |
@cookies = login(email, password) | |
end | |
def login(email, password) | |
response = connection.get("/sign_in") | |
authenticity_token = rails_auth_token(response.body) | |
cookies = response.headers['Set-Cookie'] | |
# Retrieve and store a cookie | |
params = { | |
"user[email]" => email, | |
"user[password]" => password, | |
"user[remember_me]" => 1, | |
"authenticity_token" => authenticity_token | |
} | |
response = connection.post("/sign_in", params, {'Cookie' => cookies}) | |
response.headers['Set-Cookie'] | |
end | |
def rails_auth_token(body) | |
doc = Nokogiri::HTML::Document.parse(body) | |
doc.css("form#login_form input[name='authenticity_token']").attribute("value") | |
end | |
def get_incident_id(incident_number) | |
response = connection.get("/api/beta/incidents") do |request| | |
request.params[:status] = "triggered,acknowledged" | |
end | |
doc = JSON.parse(response.body) | |
# TODO support for pagination | |
incident = doc["incidents"].find do |incident| | |
incident["incident_number"] == incident_number | |
end | |
incident["id"] if incident | |
end | |
def sumbit_event(service_key, event_type, description, | |
incident_key = nil, details = {}) | |
event = { | |
:service_key => service_key, | |
:event_type => event_type, | |
:description => description, | |
:details => details | |
} | |
event[:incident_key] = incident_key if incident_key | |
events_api_connection = Faraday.new(:url => EVENTS_URL, | |
:ssl => {:verify => false}) do |c| | |
# c.response :logger | |
c.adapter :net_http | |
end | |
events_api_connection.post(EVENTS_PATH, JSON.generate(event)) | |
end | |
def trigger_event(service_key, description, incident_key = nil, details = {}) | |
sumbit_event(service_key, "trigger", description, incident_key, details) | |
end | |
def ack_event(service_key, description, incident_key = nil, details = {}) | |
sumbit_event(service_key, "acknowledged", description, incident_key, details) | |
end | |
def resolve_event(service_key, description, incident_key = nil, details = {}) | |
sumbit_event(service_key, "resolve", description, incident_key, details) | |
end | |
def get(path, query = {}) | |
response = connection.get(path, query, {'Cookie' => cookies}).body | |
end | |
def open_incidents(service_id) | |
json_body = get("/api/beta/incidents", {"service" => service_id, | |
"status" => "triggered"}) | |
JSON.parse(json_body)["incidents"] | |
end | |
def get_service_key(service_id) | |
JSON.parse(get("/api/v1/services/#{service_id}"))["service"]["service_key"] | |
end | |
def get_raw_ile(incident_id, ile_id) | |
raw_ile_path = "/incidents/#{incident_id}/log_entries/#{ile_id}/show_raw_details" | |
get(raw_ile_path) | |
end | |
def change_status(incident_id, status) | |
body = JSON.generate({ | |
:incidents => [{:id => incident_id, :status => status}]}) | |
response = connection.put("/api/v1/incidents", body, | |
{'Cookie' => cookies, 'Content-Type' => 'application/json'}) | |
doc = JSON.parse(response.body) | |
incident = doc["incidents"].find do |incident| | |
incident["id"] == incident_id | |
end | |
return response.status == 200 | |
end | |
def ack(incident_id) | |
change_status(incident_id, "acknowledged") | |
end | |
def resolve(incident_id) | |
change_status(incident_id, "resolved") | |
end | |
end | |
class IncidentMirror | |
attr_reader :agent | |
attr_reader :original_service_id | |
attr_reader :new_service_id | |
def initialize(pagerduty_agent, original_service_id, new_service_id) | |
@agent = pagerduty_agent | |
@original_service_id = original_service_id | |
@new_service_id = new_service_id | |
end | |
def mirror | |
puts "Mirroring open incidents from service: #{original_service_id}" | |
open_incidents = agent.open_incidents(original_service_id) | |
puts "Found #{open_incidents.size} open incidents" | |
open_incidents.each do |incident| | |
incident_id = incident["id"] | |
trigger_details = incident["trigger_details"] | |
ile_id = trigger_details["id"] | |
if trigger_details["type"] == "email_trigger" | |
puts "Mirroring incident #{incident_id}" | |
raw_email = agent.get_raw_ile(incident_id, ile_id) | |
open_new_incident_with(incident_id, raw_email) | |
end | |
end | |
end | |
def open_new_incident_with(original_incident_id, email_text) | |
mail = Mail.new(email_text) | |
description = description_out_of_email(mail.body) | |
service_key = agent.get_service_key(new_service_id) | |
# Open a new incident in the API service using the description from the email | |
# incident | |
agent.trigger_event(service_key, description, original_incident_id) | |
# Resolve the original incident. However, you don't have to, since PagerDuty | |
# can also be configured to resolve incidents automatically. | |
agent.resolve(original_incident_id) | |
end | |
# Compose a description using the body of the email. Feel free to modify | |
# this method, if you know the structure of the email body | |
# | |
# mail - The String representation of the mail body | |
# | |
# Returns a description that can be used to create an API incident | |
def description_out_of_email(email) | |
# Replace all new line characters with space | |
description = email.to_s.strip.gsub(/\n|\r/, ' ') | |
# Truncate if the description is over 1024 chars (PagerDuty API Limit) | |
description = description[0..1024] if description.length > 1024 | |
description | |
end | |
end | |
# Fill in your account subdomain and user credentials here. | |
# If you don't want to share your password with anyone, we recommend that you | |
# create an API user with a shared password. | |
# Your subdomain is: http://<subdomain>.pagerduty.com | |
# | |
# Unfortunately we can't use the API tokens here as not all of PagerDuty is | |
# accessible via the tokens yet. | |
CREDENTIALS = { | |
:subdomain => "YOUR_SUBDOMAIN", | |
:email => "YOUR_EMAIL_ADDRESS", | |
:password => "PASSWORD" | |
} | |
# The service id can be obtained from here | |
# https://<subdomain>.pagerduty.com/services/<serivce-id> | |
ORIGINAL_SERVICE_ID = "PK2PZMF" # This is the Email Service that collects the original email | |
NEW_SERVICE_ID = "PVME5TH" # This is the API Service that new incident would be created on | |
agent = PagerDutyAgent.new(CREDENTIALS[:subdomain], | |
CREDENTIALS[:email], | |
CREDENTIALS[:password]) | |
im = IncidentMirror.new(agent, ORIGINAL_SERVICE_ID, NEW_SERVICE_ID) | |
im.mirror |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment