diff --git a/mass_update_incidents/README.md b/mass_update_incidents/README.md index 22c60f3..efc3498 100644 --- a/mass_update_incidents/README.md +++ b/mass_update_incidents/README.md @@ -2,8 +2,16 @@ Performs status updates (acknowledge or resolve) in bulk to an almost arbitrary number (maximum: 10k at a time) of incidents that all have an assignee user or -service (or both) in commmon. +service (or both) in commmon. If operating on more than 10k incidents: it is recommended that you run the -script several times by constraining it to a service ID each time, and/or -requesting a time range by setting the `-d/--date-range` option. +script several times by constraining it to a service ID each time. + +Alternatively, specify a time range by setting the `-d/--date-range` option. If +the difference between the beginning and end times is longer than 1 hour, the +script will run in batches of 1 hour across the time range. This is intended +to help keep each batch size within 10k incidents. + +Errors returned by the REST API are reported to stderr but do not interrupt the +script's execution, to ensure that all incidents in scope are tried at least +once each. diff --git a/mass_update_incidents/mass_update_incidents.py b/mass_update_incidents/mass_update_incidents.py index 1c1be78..4e0849d 100755 --- a/mass_update_incidents/mass_update_incidents.py +++ b/mass_update_incidents/mass_update_incidents.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 # PagerDuty Support asset: mass_update_incidents @@ -6,7 +6,8 @@ import requests import sys import json -from datetime import date +from datetime import date, datetime, timedelta +from dateutil.parser import parse, ParserError import pprint import pdpyras @@ -22,6 +23,7 @@ def mass_update_incidents(args): session = pdpyras.APISession(args.api_key, default_from=args.requester_email) + hour_slices = [] if args.user_id: PARAMETERS['user_ids[]'] = args.user_id.split(',') print("Acting on incidents assigned to user(s): "+args.user_id) @@ -35,42 +37,84 @@ def mass_update_incidents(args): elif args.action == 'acknowledge': PARAMETERS['statuses[]'] = ['triggered'] print("Acknowledging incidents") - if args.date_range is not None: + if args.date_range is None: + PARAMETERS['date_range'] = 'all' + print("Getting incidents of all time") + else: sinceuntil = args.date_range.split(',') if len(sinceuntil) != 2: raise ValueError("Date range must be two ISO8601-formatted time " "stamps separated by a comma.") - PARAMETERS['since'] = sinceuntil[0] - PARAMETERS['until'] = sinceuntil[1] + given_start_date = parse(sinceuntil[0]) + given_end_date = parse(sinceuntil[1]) + if given_end_date - given_start_date > timedelta(hours=1): + hour_slices = hour_slicer(sinceuntil[0], sinceuntil[1]) + # else: + # PARAMETERS['since'] = sinceuntil[0] + # PARAMETERS['until'] = sinceuntil[1] print("Getting incidents for date range: "+" to ".join(sinceuntil)) - else: - PARAMETERS['date_range'] = 'all' - print("Getting incidents of all time") print("Parameters: "+str(PARAMETERS)) - try: - print("Please be patient as this can take a while for large volumes " - "of incidents.") - for incident in session.list_all('incidents', params=PARAMETERS): + print("Please be patient as this can take a while for large volumes " + "of incidents.") + # if 1hr slicing is not needed, then this for loop will be no-op + for begin_date in hour_slices: + print("Now retrieving a batch of incidents...") + PARAMETERS['since'] = begin_date + PARAMETERS['until'] = begin_date + timedelta(hours=1) + incident_handler(session, args) + # if sliced into hours, this performs one final sweep through the whole + # range to catch stragglers + incident_handler(session, args) + +def hour_slicer(begin_time, end_time): + date_range = [] + start_time = parse(begin_time) + end_time = parse(end_time) + while start_time < end_time: + date_range.append(start_time) + start_time = start_time + timedelta(hours=1) + date_range.append(start_time) + return date_range + +def incident_handler(session, args): + for incident in session.list_all('incidents', params=PARAMETERS): + if args.dry_run: print("* Incident {}: {}".format(incident['id'], args.action)) - if args.dry_run: - continue - session.rput(incident['self'], json={ - 'type': 'incident_reference', - 'id': incident['id'], - 'status': '{0}d'.format(args.action), # acknowledged or resolved - }) - except pdpyras.PDClientError as e: - if e.response is not None: - print(e.response.text) - raise e + continue + if args.title_filter is None: + try: + print("* Incident {}: {}".format(incident['id'], args.action)) + session.rput(incident['self'], json={ + 'type': 'incident_reference', + 'id': incident['id'], + 'status': '{0}d'.format(args.action), + }) + except pdpyras.PDClientError as e: + if e.response is not None: + print(e.response.text) + # raise e + else: + if 'title' in incident.keys() and \ + args.title_filter in incident['title']: + print("* Incident {}: {}".format(incident['id'], args.action)) + try: + session.rput(incident['self'], json={ + 'type': 'incident_reference', + 'id': incident['id'], + 'status': '{0}d'.format(args.action), + }) + except pdpyras.PDClientError as e: + if e.response is not None: + print(e.response.text) + # raise e def main(argv=None): ap = argparse.ArgumentParser(description="Mass ack or resolve incidents " "either corresponding to a given service, or assigned to a given " "user. Note, if you are trying to update over 10k incidents at a " - "time, you should set the --date-range argument to a lesser interval " - "of time and then run this script multiple times with a different " - "interval each time until the desired range of time is covered.") + "time, you should use the --date-range argument instead. If the date " + "range given is longer than 1 hour it will be batched into 1-hour " + " slices.") ap.add_argument('-d', '--date-range', default=None, help="Only act on " "incidents within a date range. Must be a pair of ISO8601-formatted " "time stamps, separated by a comma, representing the beginning (since) " @@ -89,6 +133,9 @@ def main(argv=None): 'resolve'], help="Action to take on incidents en masse") ap.add_argument('-e', '--requester-email', required=True, help="Email " "address of the user who will be marked as performing the actions.") + ap.add_argument('-t', '--title-filter', default=None, help="(Optional) " + "string to search in the Title field of each Incident, to ensure only " + "known types of incidents are handled.") args = ap.parse_args() mass_update_incidents(args) diff --git a/mass_update_incidents/requirements.txt b/mass_update_incidents/requirements.txt index 11eb542..2f3c010 100644 --- a/mass_update_incidents/requirements.txt +++ b/mass_update_incidents/requirements.txt @@ -1 +1,3 @@ pdpyras >= 2.0.0 +python-dateutil +datetime