Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Initial commit

  • Loading branch information...
commit 5f469925aa9b779970da4e815ae1247c2ab12a4e 1 parent 9dc3ed7
David Shafer authored
5 .gitignore
View
@@ -0,0 +1,5 @@
+*.py[co]
+
+# vim
+*~
+*.swp
141 README.md
View
@@ -1,4 +1,137 @@
-phoneduty
-=========
-
-Dispatch incoming telephone voicemails/SMS messages from Twilio according to a PagerDuty on-call schedule
+phoneduty
+=========
+Dispatch incoming telephone voicemails/SMS messages from Twilio according to a PagerDuty on-call schedule
+
+Based on sample code published in February 2012 by David Hayes (see [Triggering an alert from a phone call (code sample)](http://blog.pagerduty.com/2012/02/triggering-an-alert-from-a-phone-call-code-sample/))
+
+Overview
+--------
+Uses [Google App Engine](http://appengine.google.com) with [PagerDuty](http://www.pagerduty.com) for on-call scheduling and [Twilio](http://www.twilio.com) for telephone/SMS handling.
+
+This version improves upon the sample code in a few ways:
+
+* Added support for SMS messages
+* Added support for multiple on-call numbers by passing the PagerDuty API key and voicemail greeting in the GET request
+* Updated to Python 2.7 and webapp2
+* Updated to use the full Google URL Shortener API
+
+Requirements
+------------
+1. PagerDuty account: [http://www.pagerduty.com](http://www.pagerduty.com)
+2. Twilio account: [http://www.twilio.com](http://www.twilio.com)
+3. Google App Engine account: [http://appengine.google.com](http://appengine.google.com)
+4. Google App Engine SDK for Python: [https://developers.google.com/appengine/downloads](https://developers.google.com/appengine/downloads)
+
+Usage
+-----
+The phoneduty application is configured in Twilio as a TwiML app to handle incoming telephone calls or SMS messages to a Twilio phone number.
+
+### Receive voice call:
+
+<code>GET http://{appname}.appspot.com/call?service\_key={service\_key}(&greeting={greeting})</code>
+
+Return TwiML instructing Twilio to speak a greeting to the caller and record a message. If the caller leaves
+a message, generate a PagerDuty incident for the service identified by
+`service_key`. The incident will contain the caller's phone number and a
+shortened link to the recording.
+
+Example:
+
+<code>GET http://example.appspot.com/call?service\_key=1234567890abcdef1234567890abcdef&greeting=Leave+a+message+to+contact+the+server+administrator+on+call
+
+### Receive SMS message:
+<code>GET http://{appname}.appspot.com/sms?service\_key={service\_key}</code>
+
+Generate a PagerDuty incident for the service identified by `service_key`.
+The incident will contain the sender's phone number and the SMS text.
+
+Example:
+
+<code>GET http://example.appspot.com/sms?service\_key=1234567890abcdef1234567890abcdef</code>
+
+### Options
+
+#### service\_key
+
+PagerDuty service API key; a 32-digit hexadecimal string corresponding to a PagerDuty service created using
+the "Generic API system" service type.
+
+Example:
+
+<code>service\_key=1234567890abcdef1234567890abcdef</code>
+
+#### greeting
+
+A URL-encoded text greeting to be spoken by Twilio. If not specified, the default greeting
+is "Leave a message to contact the on call staff."
+
+Example:
+
+<code>greeting=Leave+a+message+to+contact+the+server+administrator+on+call.</code>
+
+Deployment
+----------
+1. Create a new Google App Engine application, and remember the application name; example: **example.appspot.com**
+
+2. Edit `app.yaml` and change "new-project-template" to your application name, then upload the application using `appcfg.py update` (see [Uploading, Downloading, and Managing a Python App](https://developers.google.com/appengine/docs/python/tools/uploadinganapp))
+
+3. Create a new PagerDuty service using the "Generic API system" service type; note the resulting 32-digit hexadecimal Service API key; example: **1234567890abcdef1234567890abcdef**
+
+4. In Twilio, create a new TwiML App (see [Create App](https://www.twilio.com/user/account/apps/add))
+
+5. Change the Voice request type for the new app from `POST` to `GET`
+
+6. Set the Voice request URL to your application for receiving voice calls, for example (remember to replace "example" with your application name, and service\_key with your PagerDuty service API key):
+
+ <code>http://example.appspot.com/call?service\_key=1234567890abcdef1234567890abcdef</code>
+
+7. Change the SMS request type from `POST` to `GET`
+
+8. Set the SMS request URL to your application for receiving SMS messages, for example (remember to replace "example" with your application name, and service\_key with your PagerDuty service API key):
+
+ <code>http://example.appspot.com/sms?service\_key=1234567890abcdef1234567890abcdef</code>
+
+9. Try calling your application using the Twilio Client and verify it creates a PagerDuty incident successfully.
+
+10. In Twilio, create a new number and associate it with your application for Voice and SMS.
+
+11. For additional on-call numbers, use the same Google App Engine application, but specify a new PagerDuty service, TwiML app, and Twilio number.
+
+### Example: Multiple Phone Numbers
+
+#### Google App Engine Application
+* example.appspot.com (phoneduty application)
+
+#### PagerDuty Services
+* "Server Administrators Telephone (555-555-0111)" (Service API key: 11111111111111111111111111111111)
+* "Database Administrators Telephone (555-555-0122)" (Service API key: 22222222222222222222222222222222)
+
+#### Twilio TwiML Apps
+
+##### Server Administrators App
+
+Voice:
+
+<code>GET http://example.appspot.com/call?service\_key=11111111111111111111111111111111&greeting=Leave+a+message+to+page+the+server+administrator+on+call</code>
+
+SMS:
+
+<code>GET http://example.appspot.com/sms?service\_key=11111111111111111111111111111111</code>
+
+##### Database Administrators App
+
+Voice:
+
+<code>GET http://example.appspot.com/call?service\_key=22222222222222222222222222222222&greeting=Leave+a+message+to+page+the+database+administrator+on+call</code>
+
+SMS:
+
+<code>GET http://example.appspot.com/sms?service\_key=22222222222222222222222222222222</code>
+
+#### Twilio Phone Numbers
+* 555-555-0111 - Server Administrators Number
+ * Voice: `Server Administrators App`
+ * SMS: `Server Administrators App`
+* 555-555-0112 - Database Administrators Number
+ * Voice: `Database Administrators App`
+ * SMS: `Database Administrators App`
17 app.yaml
View
@@ -0,0 +1,17 @@
+application: new-project-template
+version: 1
+runtime: python27
+api_version: 1
+threadsafe: yes
+
+handlers:
+- url: /favicon\.ico
+ static_files: favicon.ico
+ upload: favicon\.ico
+
+- url: .*
+ script: main.app
+
+libraries:
+- name: django
+ version: latest
BIN  favicon.ico
View
Binary file not shown
12 index.yaml
View
@@ -0,0 +1,12 @@
+indexes:
+
+# AUTOGENERATED
+
+# This index.yaml is automatically updated whenever the dev_appserver
+# detects that a new type of query is run. If you want to manage the
+# index.yaml file manually, remove the above marker line (the line
+# saying "# AUTOGENERATED"). If you want to manage some indexes
+# manually, move them above the marker line. The index.yaml file is
+# automatically uploaded to the admin console when you next deploy
+# your application using appcfg.py.
+
150 main.py
View
@@ -0,0 +1,150 @@
+# phoneduty
+# Dispatch incoming telephone voicemails/SMS messages from Twilio according to a PagerDuty on-call schedule
+
+from google.appengine.ext import webapp
+from google.appengine.ext.webapp.util import run_wsgi_app
+import json
+import logging
+import urllib2
+import urlparse
+
+# Shorten MP3 URL for SMS length limits
+def shorten(url):
+ gurl = 'https://www.googleapis.com/urlshortener/v1/url'
+ data = json.dumps({'longUrl': url})
+ request = urllib2.Request(gurl, data, {'Content-Type': 'application/json'})
+ try:
+ f = urllib2.urlopen(request)
+ results = json.load(f)
+ except urllib2.HTTPError, e: # triggers on HTTP code 201
+ logging.warn(e.code)
+ error_content = e.read()
+ results = json.JSONDecoder().decode(error_content)
+ return results['id']
+
+# Outbput TwilML to record a message and pass it to the RecordHandler
+class CallHandler(webapp.RequestHandler):
+ def get(self):
+ logging.info('Recieved call: ' + self.request.query_string)
+
+ # Set service key
+ if (self.request.get("service_key")):
+ service_key = self.request.get("service_key")
+ logging.debug("service_key = \"" + service_key + "\"")
+ else:
+ logging.error("No service key specified")
+
+ # Set greeting
+ if (self.request.get("greeting")):
+ greeting = self.request.get("greeting")
+ logging.debug("greeting = \"" + greeting + "\"")
+ else:
+ logging.info("Using default greeting")
+ greeting = "Leave a message to contact the on call staff."
+
+ # Determine the RecordHandler URL to use based on the current base URL
+ o = urlparse.urlparse(self.request.url)
+ recordURL = urlparse.urlunparse((o.scheme, o.netloc, 'record?service_key=' + service_key, '', '', ''))
+ logging.debug("recordURL = \"" + recordURL + "\"")
+
+ response = (
+ "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>"
+ "<Response>"
+ " <Say>" + greeting + "</Say>"
+ " <Record action=\"" + recordURL + "\" method=\"GET\"/>"
+ " <Say>I did not receive a recording.</Say>"
+ "</Response>")
+ logging.debug("response = \"" + response + "\"")
+ self.response.out.write(response)
+
+# Open a PagerDuty incident based on an SMS message
+class SMSHandler(webapp.RequestHandler):
+ def get(self):
+ logging.info('Received SMS: ' + self.request.query_string)
+
+ # Set service key
+ if (self.request.get("service_key")):
+ service_key = self.request.get("service_key")
+ logging.debug("service_key = \"" + service_key + "\"")
+ else:
+ logging.error("No service key specified")
+
+ incident = '{"service_key": "%s","incident_key": "%s","event_type": "trigger","description": "%s %s"}'%(service_key,self.request.get("From"),self.request.get("From"),self.request.get("Body"))
+
+ try:
+ r = urllib2.Request("http://events.pagerduty.com/generic/2010-04-15/create_event.json", incident) #Note according to the API this should be retried on failure
+ results = urllib2.urlopen(r)
+ logging.debug(incident)
+ logging.debug(results)
+ except urllib2.HTTPError, e:
+ logging.warn( e.code )
+ except urllib2.URLError, e:
+ logging.warn(e.reason)
+
+# Shorten the URL and trigger a PagerDuty incident
+class RecordHandler(webapp.RequestHandler):
+ def get(self):
+ logging.info('Received recording: ' + self.request.query_string)
+
+ # Set service key
+ if (self.request.get("service_key")):
+ service_key = self.request.get("service_key")
+ logging.debug("service_key = \"" + service_key + "\"")
+ else:
+ logging.error("No service key specified")
+
+ recUrl = self.request.get("RecordingUrl")
+ phonenumber = self.request.get("From")
+
+ response = (
+ "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
+ "<Response><Say>Thank you. We are now directing your message to the on call staff. Goodbye.</Say>"
+ "</Response>")
+ self.response.out.write(response)
+
+ if(recUrl):
+ logging.debug('Recording URL: ' + recUrl)
+ recUrl = recUrl + '.mp3' # Append .mp3 to improve playback on more devices
+ else:
+ logging.warn('No recording URL found')
+ recUrl = ""
+
+ shrten = "Error"
+ try:
+ shrten = shorten(recUrl)
+ except urllib2.HTTPError, e:
+ shrten = "HTTPError"
+ logging.warn( e.code )
+ except urllib2.URLError, e:
+ shrten = "URLError"
+ logging.warn(e.reason)
+
+ logging.info('Shortened to: ' + shrten)
+
+ incident = '{"service_key": "%s","incident_key": "%s","event_type": "trigger","description": "%s %s"}'%(service_key,shrten,shrten,phonenumber)
+ try:
+ r = urllib2.Request("http://events.pagerduty.com/generic/2010-04-15/create_event.json", incident) #Note according to the API this should be retried on failure
+ results = urllib2.urlopen(r)
+ logging.debug(incident)
+ logging.debug(results)
+ except urllib2.HTTPError, e:
+ logging.warn( e.code )
+ except urllib2.URLError, e:
+ logging.warn(e.reason)
+
+# A somewhat descriptive index page
+class IndexHandler(webapp.RequestHandler):
+ def get(self):
+ response = (
+ "<html>"
+ "<h1>phoneduty</h1>"
+ "<p><a href=\"http://www.github.com/dsshafer/phoneduty\">http://www.github.com/dsshafer/phoneduty</a></p>"
+ "</html>")
+ self.response.out.write(response)
+
+app = webapp.WSGIApplication([
+ ('/call', CallHandler),
+ ('/record', RecordHandler),
+ ('/sms', SMSHandler),
+ ('/', IndexHandler)],
+ debug=True)
Please sign in to comment.
Something went wrong with that request. Please try again.