Skip to content

Commit

Permalink
Initial release
Browse files Browse the repository at this point in the history
  • Loading branch information
brucep-care committed Oct 9, 2014
1 parent 756f76f commit 14ab943
Show file tree
Hide file tree
Showing 4 changed files with 381 additions and 1 deletion.
112 changes: 111 additions & 1 deletion README.md
@@ -1,4 +1,114 @@
=======
pdmaint
=======

A command line utility for scheduling and managing maintenance windows in PagerDuty
A command line utility for scheduling and managing maintenance windows in PagerDuty.

Requirements
============

pdmaint uses the python ``pygerduty`` library to interact with the PagerDuty API.
This library can be installed from `source <https://github.com/dropbox/pygerduty>`_ or via ``pip install pygerduty``.

pdmaint also makes use of the ``pytz`` library for timezone data.
This library can be installed from `source <http://pytz.sourceforge.net/>`_ or via ``pip install pytz``.

Documentation
=============

To get started with pdmaint, copy the file pdmaint-sample to ~/.pdmaint and
edit the [pagerduty] section. Include an API key for your PagerDuty account,
an e-mail address associated with your PagerDuty account, and the name of
your PagerDuty account. If you access PagerDuty via the url
https://blargh.pagerduty.com then your account name is blargh.

Also make sure to set your time zone properly. It should match the time zone
specified in your Pager Duty account. For a list of time zone strings simply
run the tzlist command.

Once the [pagerduty] section of your ~/.pdmaint file is set up then invoke
pdmaint with the "services" command and you should see output that
summarizes your defined services. It should look something like this:

$ pdmaint services
Service name Service ID
---------------------------------------------------
Production Alerts TV4ATV1
Staging Alerts TQDX1FV
Test services TT42DPX

You can now schedule maintenance for any of these services using the pdmaint
command line:

$ pdmaint schedule -d 30 -n "30 minute test" -i TT42DPX
ID: TX3JWSD : 30 minute test
created by: Bruce Pennypacker
start time: Thu Oct 9 13:18:47 2014 -0400
end time : Thu Oct 9 13:48:46 2014 -0400
services:
Test services

And to end the maintenance window on PagerDuty:

$ pdmaint delete TX3JWSD

To schedule multiple services for maintenance just specify multiple service
IDs separated by commas:

$ pdmaint schedule -d 60 -n "maintenance work" -i TV4ATV1,TQDX1FV,TT42DPX
ID: TP3DXQ1 : 30 minute test
created by: Bruce Pennypacker
start time: Thu Oct 9 13:23:11 2014 -0400
end time : Thu Oct 9 13:53:09 2014 -0400
services:
Production Alerts
Staging Alerts

If you find yourself reguarly scheduling similar maintenance windows then
you can define templates in your ~/.pdmaint configuration file. Sections in
the configuration file whose name ends with _schedule define templates
that let you quickly and easily schedule common maintenance windows. For
example, the folloing block defines a 2 hour maintenance window for
performing releases to two services and a 30 minute window for testing another.

[release_schedule]
duration=120
note=Release maintenance window
ids=TV4ATV1,TQDX1FV

[test_schedule]
duration=30
note=Testing
ids=TT42DPX


With the above entry in your ~/.pdmaint you can now do the following:

Get a list of defined schedules:

$ pdmaint schedules
release
test

Schedule a release maintenance window:

$ pdmaint schedule release
ID: TJP53AF : maintenance window
created by: Bruce Pennypacker
start time: Thu Oct 9 13:43:54 2014 -0400
end time : Thu Oct 9 14:13:53 2014 -0400
services:
Production Alerts
Staging Alerts

If desired you can override a defined maintenance window. Suppose you know
that a release will take an hour longer than usual:

$ pdmaint schedule release -d 180
ID: TJQ51A5 : maintenance window
created by: Bruce Pennypacker
start time: Thu Oct 9 13:47:33 2014 -0400
end time : Thu Oct 9 16:47:32 2014 -0400
services:
Production Alerts
Staging Alerts
230 changes: 230 additions & 0 deletions pdmaint
@@ -0,0 +1,230 @@
#!/usr/bin/env python

#The MIT License (MIT)
#
#Copyright (c) 2014 Care.com
#
#Permission is hereby granted, free of charge, to any person obtaining a copy
#of this software and associated documentation files (the "Software"), to deal
#in the Software without restriction, including without limitation the rights
#to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
#copies of the Software, and to permit persons to whom the Software is
#furnished to do so, subject to the following conditions:
#
#The above copyright notice and this permission notice shall be included in all
#copies or substantial portions of the Software.
#
#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
#IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
#FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
#AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
#LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
#OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
#SOFTWARE.

# Based on the following:
# https://github.com/dropbox/pygerduty
# http://developer.pagerduty.com/documentation/rest

import os
import sys
import argparse
import ConfigParser
import pygerduty
import datetime
from pytz import timezone
import dateutil.parser

try:
import json
except ImportError:
import simplejson as json

def print_window(window):
stime = dateutil.parser.parse(window.start_time)
etime = dateutil.parser.parse(window.end_time)
desc = window.description
if desc == None:
desc = ''
else:
desc = ': ' + desc
print "ID: %s %s" % (window.id, desc)
print "created by: %s" % (window.created_by.name)
print "start time: %s" % (stime.strftime("%c %z"))
print "end time : %s" % (etime.strftime("%c %z"))
print "services:"
for s in window.services:
print " %s" % (s.name)
print ""

def do_services(pager, args, config):
print "Service name Service ID"
print "---------------------------------------------------"
for service in pager.services.list():
print "%-40s %s" % (service.name, service.id)

def do_list(pager, args, config):
# filter can be past, future, or ongoing. We don't care about
# maintenance windows that occurred in the past, so only display
# ongoing and then future maintenance windows
# See http://developer.pagerduty.com/documentation/rest/maintenance_windows/list
for window in pager.maintenance_windows.list(filter="ongoing"):
print_window(window)

for window in pager.maintenance_windows.list(filter="future"):
print_window(window)

def do_users(pager, args, config):
print "Username e-mail userid"
print "---------------------------------------------------------------------"
for user in pager.users.list():
print "%-20s %-40s %s" % (user.name, user.email, user.id)

def do_schedules(pager, args, config):
for s in sorted(config.sections()):
if s.endswith('_schedule'):
print s[:-9]

def do_schedule(pager, args, config):
# First we have to find the PD userid based on the users e-mail
# address
userid = None
for user in pager.users.list():
if user.email == email:
userid = user.id
break

if userid == None:
print "e-mail address %s not recognized in PagerDuty account %s" \
% (email, account)
sys.exit(1)

duration = None
note = None
ids = None

# If args.option is defined then read default values from
# the configuration file
if args.option:
cfg = args.option + "_schedule"
if config.has_option(cfg, 'duration'):
duration = config.get(cfg, 'duration')
if config.has_option(cfg, 'note'):
note = config.get(cfg, 'note')
if config.has_option(cfg, 'ids'):
ids = config.get(cfg, 'ids')

now = datetime.datetime.now(tz=TZ)
if args.duration:
duration = args.duration

try:
end = now + datetime.timedelta(0,0,0,0,int(duration))
except Exception, e:
print "Missing or invalid duration"
sys.exit(1)

if args.note:
note = args.note

if note == None:
note=""

if args.ids:
ids = args.ids

if not ids:
print "Missing ID list"
sys.exit(1)

try:
res = pager.maintenance_windows.create(start_time=now.isoformat(),
end_time=end.isoformat(),
service_ids=ids.split(","),
requester_id=userid,
description=note)
except Exception, e:
print "Error submitting maintenance window request"
print e
sys.exit(1)

print_window(res)

def do_delete(pager, args, config):
if not args.option:
print "Missing ID to delete"
sys.exit(1)

try:
res = pager.maintenance_windows.delete(args.option)
except Exception, e:
print e
sys.exit(1)

if __name__ == '__main__':
parser = argparse.ArgumentParser(description="PagerDuty Maintenance utility")
parser.add_argument('-c', '--config',
help="Configuration file. Default: ~/.pdmaint",
required=False,
default='~/.pdmaint')
parser.add_argument('-d', '--duration',
help="Duration in minutes of the scheduled downtime",
required=False)
parser.add_argument('-n', '--note',
help="Optional note associated with the maintenance window",
required=False)
parser.add_argument('-i', '--ids',
help="List of service ID's. Use 'services' command for a list of valid ID's",
required=False)
parser.add_argument('cmd', help="One of: services, users, schedule, list, delete, schedules")
parser.add_argument('option', nargs='?', help="When cmd=schedule this can be an optional pre-defined schedule read from the configuration file. When cmd=delete this must contain the ID of the maintenance window to delete.")

args = parser.parse_args()

config = ConfigParser.ConfigParser()
configfile = os.path.realpath(os.path.expanduser(args.config))

if os.path.isfile(configfile):
try:
config.read([configfile])
except Exception, e:
print e
sys.exit(1)
else:
print "File %s does not exist" % (configfile)
sys.exit(1)

# The conf file should look something like this:
# [pagerduty]
# api_key=abcdefghijklmnopqrst
# email=someuser@somedomain.com
# account=pagerduty_account_key
# timezone=US/Eastern

for p in [ 'api_key', 'email', 'account', 'timezone' ]:
if not config.has_option('pagerduty', p):
print "'%s' not defined pagerduty section of %s" % (p, args.config)
sys.exit(1)

api_key = config.get('pagerduty', 'api_key')
email = config.get('pagerduty', 'email')
account = config.get('pagerduty', 'account')
TZ = timezone(config.get('pagerduty', 'timezone'))

pager = pygerduty.PagerDuty(account, api_key)

if args.cmd == 'services':
do_services(pager, args, config)
elif args.cmd == 'list':
do_list(pager, args, config)
elif args.cmd == 'users':
do_users(pager, args, config)
elif args.cmd == 'schedule':
do_schedule(pager, args, config)
elif args.cmd == 'schedules':
do_schedules(pager, args, config)
elif args.cmd == 'delete':
do_delete(pager, args, config)
else:
print "Unknown command: %s" % (args.cmd)
sys.exit(1)
34 changes: 34 additions & 0 deletions pdmaint-sample
@@ -0,0 +1,34 @@
# Copy this file to ~/.pdmaint and edit as necessary. All the entries in the
# # [pagerduty] section are required. All subsequent sections with a _schedule
# prefix are optional.

[pagerduty]
# PagerDuty API key. To obtain a key visit https://<your_account>.pagerduty.com/api_keys
api_key=xxxxxxxxxxxxxxxxxxxx

# e-mail address associated with a PagerDuty account
email=you@yourdomain.com

# The name of your account. From: https://your_account.pagerduty.com/
account=your_account

# Your local timezone. The tzinfo utility can be used to see all valid values.
timezone=US/Eastern


# The following ar eexamples of defined schedules. Use the command 'pdmaint services'
# to obtain a list of service ID's.

# Schedule 2 hours of downtime when performing a release. Disable a bunch of
# service IDs
[release_schedule]
duration=120
note=Production release
ids=XYZ1234,A1B2C3D,XXXXXXX,YYYYYYY

# Schedule a 30 minute maintenance window for one service
[maintenance_schedule]
duration=30
note=maintenance window
ids=DK3X403

6 changes: 6 additions & 0 deletions tzlist
@@ -0,0 +1,6 @@
#!/usr/bin/env python

import pytz

for tz in pytz.all_timezones:
print tz

0 comments on commit 14ab943

Please sign in to comment.