def on_get(self, req, resp, source, **kwargs): """ Retrieve the list of flows collected on a given SourceIP. Additionally it can be provided the DestinationIP and DestinationPort. This method accepts also the start_time and end_time as query parameters :param source: SourceIP of the flow. :param kwargs: It can be: only DestinationIP or DestinationIP and DestinationPort :return: List with flows """ dimensions_query = 'SourceIP:' + source if kwargs: dimensions_query += ',' + ','.join(['{}:{}'.format(key, value) for key, value in kwargs.items()]) req.context['query_parameters']['dimensions'] = dimensions_query if 'metric' not in req.context['query_parameters']: raise HTTPBadRequest(title='Missing Metric', description='Missing metric name to collect metrics', code='300') req.context['query_parameters']['name'] = req.context['query_parameters'].get('metric') req.context['query_parameters']['group_by'] = 'SourceIP,DestinationIP,DestinationPort,FlowID' req.context['query_parameters']['merge_metrics'] = 'true' if 'start_time' not in req.context['query_parameters']: d = datetime.now() - timedelta(days=1) req.context['query_parameters']['start_time'] = d.isoformat() headers = Authenticate.get_header() # Insert the auth header r = request(ConfReader().get('MONASCA', 'url') + Flows.ENDPOINT, params=req.context['query_parameters'], headers=headers) req.context['query_parameters'].pop('group_by') req.context['query_parameters'].pop('merge_metrics') req.context['query_parameters'].pop('name') resp.body = self.format_body(Flows.__convert_result__(r.json(), req.uri, req), from_dict=True) resp.status = str(r.status_code)
async def on_post(self, req, res): try: result = await self.graphql_execute(req, res) image_info = req.context.get('image_info', None) if not result.errors: res.status = HTTP_200 res.body = json.dumps(result.data) else: if image_info: for image in image_info: if os.path.isfile(image['full_path']): os.remove(image['full_path']) raise CodeduExceptionHandler(result.errors[0].args[0]) print(result.errors) except Exception as e: description = e.description if hasattr( e, 'description') else 'UNKNOWN ERROR' if hasattr(e, 'type'): raise globals()[e.type](description=description) else: print_exc() raise HTTPBadRequest(description=e.args)
def on_put(req, resp, notification_id): ''' Edit user notification settings. Allows editing of the following attributes: - roles: list of role names - team: team name - mode: contact mode name - type: string defining what event to notify on. Types are detailed in notification POST documentation - time_before: in units of seconds (if reminder setting) - only_if_involved: boolean (if notification setting) **Example request** .. sourcecode:: http PUT /api/v0/events/1234 HTTP/1.1 Content-Type: application/json { "team": "team-bar", "mode": "call", "user": "******", "roles": ["secondary"] } :statuscode 200: Successful edit :statuscode 400: Validation checks failed. ''' data = load_json_body(req) params = data.keys() roles = data.pop('roles') cols = [columns[c] for c in data if c in columns] query_params = [data[c] for c in params if c in columns] query = 'UPDATE notification_setting SET %s WHERE id = %%s' % ', '.join( cols) connection = db.connect() cursor = connection.cursor(db.DictCursor) try: notification_type = data.get('type') cursor.execute( '''SELECT `is_reminder`, `time_before`, `only_if_involved` FROM `notification_setting` JOIN `notification_type` ON `notification_setting`.`type_id` = `notification_type`.`id` WHERE `notification_setting`.`id` = %s''', notification_id) current_setting = cursor.fetchone() is_reminder = current_setting['is_reminder'] if notification_type: cursor.execute( 'SELECT is_reminder FROM notification_type WHERE name = %s', notification_type) is_reminder = cursor.fetchone()['is_reminder'] time_before = data.get('time_before', current_setting['time_before']) only_if_involved = data.get('only_if_involved', current_setting['only_if_involved']) if is_reminder and only_if_involved is not None: raise HTTPBadRequest( 'invalid setting update', 'reminder setting must define only time_before') elif not is_reminder and time_before is not None: raise HTTPBadRequest( 'invalid setting update', 'notification setting must define only only_if_involved') if cols: cursor.execute( 'SELECT `user`.`name` FROM `notification_setting` ' 'JOIN `user` ON `notification_setting`.`user_id` = `user`.`id` ' 'WHERE `notification_setting`.`id` = %s', notification_id) username = cursor.fetchone()['name'] check_user_auth(username, req) cursor.execute(query, query_params + [notification_id]) if roles: cursor.execute( 'DELETE FROM `setting_role` WHERE `setting_id` = %s', notification_id) query_vals = ', '.join([ '(%s, (SELECT `id` FROM `role` WHERE `name` = %%s))' % notification_id ] * len(roles)) cursor.execute( 'INSERT INTO `setting_role`(`setting_id`, `role_id`) VALUES ' + query_vals, roles) except: raise else: connection.commit() finally: cursor.close() connection.close()
def load_json_body(req): try: return json_loads(req.context['body']) except ValueError as e: raise HTTPBadRequest('invalid JSON', 'failed to decode json: %s' % str(e))
def on_post(self, req, resp): form_body = uri.parse_query_string(req.context['body']) try: template_subject = form_body['templateSubject'] template_body = form_body['templateBody'] application = form_body['application'] except KeyError: raise HTTPBadRequest('Missing keys from post body', '') if not application: resp.body = ujson.dumps({'error': 'No application found'}) resp.status = falcon.HTTP_400 return app_json = get_local(req, 'applications/%s' % application) sample_context_str = app_json.get('sample_context') if not sample_context_str: resp.body = ujson.dumps( {'error': 'Missing sample_context from application config'}) resp.status = falcon.HTTP_400 logger.error('Missing sample context for app %s', application) return try: sample_context = ujson.loads(sample_context_str) except Exception: resp.body = ujson.dumps( {'error': 'Invalid application sample_context'}) resp.status = falcon.HTTP_400 logger.exception('Bad sample context for app %s', application) return # TODO: also move iris meta var to api iris_sample_context = { "message_id": 5456900, "target": "user", "priority": "Urgent", "application": "Autoalerts", "plan": "default plan", "plan_id": 1843, "incident_id": 178293332, "template": "default template" } sample_context['iris'] = iris_sample_context environment = SandboxedEnvironment() try: subject_template = environment.from_string(template_subject) body_template = environment.from_string(template_body) except Exception as e: resp.body = ujson.dumps({'error': str(e), 'lineno': e.lineno}) resp.status = falcon.HTTP_400 else: resp.body = ujson.dumps({ 'template_subject': subject_template.render(sample_context), 'template_body': body_template.render(sample_context) })
def get_schedules(filter_params, dbinfo=None, fields=None): """ Helper function to get schedule data for a request. :param filter_params: dict mapping constraint keys with values. Valid constraints are defined in the global ``constraints`` dict. :param dbinfo: optional. If provided, defines (connection, cursor) to use in DB queries. Otherwise, this creates its own connection/cursor. :param fields: optional. If provided, defines which schedule fields to return. Valid fields are defined in the global ``columns`` dict. Defaults to all fields. Invalid fields raise a 400 Bad Request. :return: """ events = False scheduler = False from_clause = ['`schedule`'] if fields is None: fields = columns.keys() if any(f not in columns for f in fields): raise HTTPBadRequest('Bad fields', 'One or more invalid fields') if 'roster' in fields: from_clause.append('JOIN `roster` ON `roster`.`id` = `schedule`.`roster_id`') if 'team' in fields or 'timezone' in fields: from_clause.append('JOIN `team` ON `team`.`id` = `schedule`.`team_id`') if 'role' in fields: from_clause.append('JOIN `role` ON `role`.`id` = `schedule`.`role_id`') if 'scheduler' in fields: from_clause.append('JOIN `scheduler` ON `scheduler`.`id` = `schedule`.`scheduler_id`') scheduler = True if 'events' in fields: from_clause.append('LEFT JOIN `schedule_event` ON `schedule_event`.`schedule_id` = `schedule`.`id`') events = True if 'id' not in fields: fields.append('id') fields = map(columns.__getitem__, fields) cols = ', '.join(fields) from_clause = ' '.join(from_clause) connection_opened = False if dbinfo is None: connection = db.connect() connection_opened = True cursor = connection.cursor(db.DictCursor) else: connection, cursor = dbinfo where = ' AND '.join(constraints[key] % connection.escape(value) for key, value in filter_params.iteritems() if key in constraints) query = 'SELECT %s FROM %s' % (cols, from_clause) if where: query = '%s WHERE %s' % (query, where) cursor.execute(query) data = cursor.fetchall() if scheduler and data: schedule_ids = {d['id'] for d in data} cursor.execute('''SELECT `schedule_id`, `user`.`name` FROM `schedule_order` JOIN `user` ON `user_id` = `user`.`id` WHERE `schedule_id` IN %s ORDER BY `schedule_id`,`priority`, `user_id`''', schedule_ids) orders = {} # Accumulate roster orders for schedule for row in cursor: schedule_id = row['schedule_id'] if schedule_id not in orders: orders[schedule_id] = [] orders[schedule_id].append(row['name']) if connection_opened: cursor.close() connection.close() # Format schedule events if events: # end result accumulator ret = {} for row in data: schedule_id = row.pop('schedule_id') # add data row into accumulator only if not already there if schedule_id not in ret: ret[schedule_id] = row ret[schedule_id]['events'] = [] start = row.pop('start') duration = row.pop('duration') ret[schedule_id]['events'].append({'start': start, 'duration': duration}) data = ret.values() if scheduler: for schedule in data: scheduler_data = {'name': schedule['scheduler']} if schedule['id'] in orders: scheduler_data['data'] = orders[schedule['id']] schedule['scheduler'] = scheduler_data return data
def on_get(req, resp): """ Search for events. Allows filtering based on a number of parameters, detailed below. **Example request**: .. sourcecode:: http GET /api/v0/events?team=foo-sre&end__gt=1487466146&role=primary HTTP/1.1 Host: example.com **Example response**: .. sourcecode:: http HTTP/1.1 200 OK Content-Type: application/json [ { "start": 1488441600, "end": 1489132800, "team": "foo-sre", "link_id": null, "schedule_id": null, "role": "primary", "user": "******", "full_name": "Foo Icecream", "id": 187795 }, { "start": 1488441600, "end": 1489132800, "team": "foo-sre", "link_id": "8a8ae77b8c52448db60c8a701e7bffc2", "schedule_id": 123, "role": "primary", "user": "******", "full_name": "Bar Apple", "id": 187795 } ] :query team: team name :query user: user name :query role: role name :query id: id of the event :query start: start time (unix timestamp) of event :query end: end time (unix timestamp) of event :query start__gt: start time (unix timestamp) greater than :query start__ge: start time (unix timestamp) greater than or equal :query start__lt: start time (unix timestamp) less than :query start__le: start time (unix timestamp) less than or equal :query end__gt: end time (unix timestamp) greater than :query end__ge: end time (unix timestamp) greater than or equal :query end__lt: end time (unix timestamp) less than :query end__le: end time (unix timestamp) less than or equal :query role__eq: role name :query role__contains: role name contains param :query role__startswith: role name starts with param :query role__endswith: role name ends with param :query team__eq: team name :query team__contains: team name contains param :query team__startswith: team name starts with param :query team__endswith: team name ends with param :query team_id: team id :query user__eq: user name :query user__contains: user name contains param :query user__startswith: user name starts with param :query user__endswith: user name ends with param :statuscode 200: no error :statuscode 400: bad request """ fields = req.get_param_as_list('fields') if fields: fields = [columns[f] for f in fields if f in columns] req.params.pop('fields', None) include_sub = req.get_param_as_bool('include_subscribed') if include_sub is None: include_sub = True req.params.pop('include_subscribed', None) cols = ', '.join(fields) if fields else all_columns if any(key not in constraints for key in req.params): raise HTTPBadRequest('Bad constraint param') query = '''SELECT %s FROM `event` JOIN `user` ON `user`.`id` = `event`.`user_id` JOIN `team` ON `team`.`id` = `event`.`team_id` JOIN `role` ON `role`.`id` = `event`.`role_id`''' % cols where_params = [] where_vals = [] connection = db.connect() cursor = connection.cursor(db.DictCursor) # Build where clause. If including subscriptions, deal with team parameters later params = req.params.keys() - TEAM_PARAMS if include_sub else req.params for key in params: val = req.get_param(key) if key in constraints: where_params.append(constraints[key]) where_vals.append(val) # Deal with team subscriptions and team parameters team_where = [] subs_vals = [] team_params = req.params.keys() & TEAM_PARAMS if include_sub and team_params: for key in team_params: val = req.get_param(key) team_where.append(constraints[key]) subs_vals.append(val) subs_and = ' AND '.join(team_where) cursor.execute( '''SELECT `subscription_id`, `role_id` FROM `team_subscription` JOIN `team` ON `team_id` = `team`.`id` WHERE %s''' % subs_and, subs_vals) if cursor.rowcount != 0: # Build where clause based on team params and subscriptions subs_and = '(%s OR (%s))' % (subs_and, ' OR '.join([ '`team`.`id` = %s AND `role`.`id` = %s' % (row['subscription_id'], row['role_id']) for row in cursor ])) where_params.append(subs_and) where_vals += subs_vals where_query = ' AND '.join(where_params) if where_query: query = '%s WHERE %s' % (query, where_query) cursor.execute(query, where_vals) data = cursor.fetchall() cursor.close() connection.close() resp.body = json_dumps(data)
def validate_post(self, body): if not all(k in body for k in ("event_id", "details")): raise HTTPBadRequest('missing event_id and/or details attributes')
class ParseException(Exception): """ An exception to raise if an error occurs while parsing an opinion page """ parse_error = HTTPInternalServerError({ "title": "Internal server error", "description": "The opinion could not be parsed." }) not_understood = HTTPBadRequest({ "title": "Bad request", "description": "The request was not understood." }) not_found = HTTPNotFound() internal_server_error = HTTPInternalServerError({ "title": "Internal server error", "description": "The opinion could not be retrieved." }) missing_url_param = HTTPBadRequest({ "title": "Bad request",
def on_post(req, resp): """ Swap events. Takes an object specifying the 2 events to be swapped. Swap can take either single events or event sets, depending on the value of the "linked" attribute. If "linked" is True, the API interprets the "id" attribute as a link_id. Otherwise, it's assumed to be an event_id. Note that this allows swapping a single event with a linked event. **Example request**: .. sourcecode:: http POST api/v0/events/swap HTTP/1.1 Content-Type: application/json { "events": [ { "id": 1, "linked": false }, { "id": "da515a45e2b2467bbdc9ea3bc7826d36", "linked": true } ] } :statuscode 200: Successful swap :statuscode 400: Validation checks failed """ data = load_json_body(req) try: ev_0, ev_1 = data['events'] except ValueError: raise HTTPBadRequest('Invalid event swap request', 'Must provide 2 events') connection = db.connect() cursor = connection.cursor(db.DictCursor) try: # Accumulate event info for each link/event id events = [None, None] for i, ev in enumerate([ev_0, ev_1]): if not ev.get('id'): raise HTTPBadRequest('Invalid event swap request', 'Invalid event id: %s' % ev.get('id')) if ev.get('linked'): cursor.execute( 'SELECT `id`, `start`, `end`, `team_id`, `user_id`, `role_id`, ' '`link_id` FROM `event` WHERE `link_id` = %s', ev['id']) else: cursor.execute( 'SELECT `id`, `start`, `end`, `team_id`, `user_id`, `role_id`, ' '`link_id` FROM `event` WHERE `id` = %s', ev['id']) if cursor.rowcount == 0: raise HTTPNotFound() events[i] = cursor.fetchall() events_0, events_1 = events events = events_0 + events_1 # Validation checks now = time.time() if any([ev['start'] < now - constants.GRACE_PERIOD for ev in events]): raise HTTPBadRequest('Invalid event swap request', 'Cannot edit events in the past') if len(set(ev['team_id'] for ev in events)) > 1: raise HTTPBadRequest( 'Event swap not allowed', 'Swapped events must come from the same team') for ev_list in [events_0, events_1]: if len(set([ev['user_id'] for ev in ev_list])) != 1: raise HTTPBadRequest( '', 'all linked events must have the same user') check_calendar_auth_by_id(events[0]['team_id'], req) # Swap event users change_queries = [] for ev in (ev_0, ev_1): if not ev['linked']: # Break link if swapping a single event in a linked chain change_queries.append( 'UPDATE `event` SET `user_id` = %s, `link_id` = NULL WHERE `id` IN %s' ) else: change_queries.append( 'UPDATE `event` SET `user_id` = %s WHERE `id` IN %s') user_0 = events_0[0]['user_id'] user_1 = events_1[0]['user_id'] first_event_0 = min(events_0, key=lambda ev: ev['start']) first_event_1 = min(events_1, key=lambda ev: ev['start']) cursor.execute(change_queries[0], (user_1, [e0['id'] for e0 in events_0])) cursor.execute(change_queries[1], (user_0, [e1['id'] for e1 in events_1])) cursor.execute('SELECT id, full_name FROM user WHERE id IN %s', ([user_0, user_1], )) full_names = {row['id']: row['full_name'] for row in cursor} cursor.execute('SELECT name FROM team WHERE id = %s', events[0]['team_id']) team_name = cursor.fetchone()['name'] context = { 'full_name_0': full_names[user_0], 'full_name_1': full_names[user_1], 'team': team_name } create_notification(context, events[0]['team_id'], {events_0[0]['role_id'], events_1[0]['role_id']}, EVENT_SWAPPED, [user_0, user_1], cursor, start_time_0=first_event_0['start'], start_time_1=first_event_1['start']) create_audit( { 'request_body': data, 'events_swapped': (events_0, events_1) }, team_name, EVENT_SWAPPED, req, cursor) connection.commit() except HTTPError: raise else: connection.commit() finally: cursor.close() connection.close()
def _bad_request(): description = 'Mandatory params missing from the request. ' \ 'Please check your request params and retry' logger.exception(description) raise HTTPBadRequest("HTTP Bad Request", description)
def on_post(req, resp, team): ''' Escalate to a team using Iris. Configured in the 'iris_plan_integration' section of the configuration file. Escalation plan is specified via keyword, currently: 'urgent', 'medium', or 'custom'. These keywords correspond to the plan specified in the iris_plan_integration urgent_plan key, the iris integration medium_plan key, and the team's iris plan defined in the DB, respectively. If no plan is specified, the team's custom plan will be used. If iris plan integration is not activated, this endpoint will be disabled. **Example request:** .. sourcecode:: http POST /v0/events HTTP/1.1 Content-Type: application/json { "description": "Something bad happened!", "plan": "urgent" } :statuscode 200: Incident created :statuscode 400: Escalation failed, missing description/No escalation plan specified for team/Iris client error. ''' data = load_json_body(req) plan = data.get('plan') dynamic = False if plan == URGENT: plan_settings = iris.settings['urgent_plan'] dynamic = True elif plan == MEDIUM: plan_settings = iris.settings['medium_plan'] dynamic = True elif plan == CUSTOM or plan is None: # Default to team's custom plan for backwards compatibility connection = db.connect() cursor = connection.cursor() cursor.execute('SELECT iris_plan FROM team WHERE name = %s', team) if cursor.rowcount == 0: cursor.close() connection.close() raise HTTPBadRequest( 'Iris escalation failed', 'No escalation plan specified ' 'and team has no custom escalation plan defined') plan_name = cursor.fetchone()[0] cursor.close() connection.close() else: raise HTTPBadRequest('Iris escalation failed', 'Invalid escalation plan') requester = req.context.get('user') if not requester: requester = req.context['app'] data['requester'] = requester if 'description' not in data or data['description'] == '': raise HTTPBadRequest('Iris escalation failed', 'Escalation cannot have an empty description') try: if dynamic: plan_name = plan_settings['name'] targets = plan_settings['dynamic_targets'] for t in targets: # Set target to team name if not overridden in settings if 'target' not in t: t['target'] = team re = iris.client.post(iris.client.url + 'incidents', json={ 'plan': plan_name, 'context': data, 'dynamic_targets': targets }) re.raise_for_status() incident_id = re.json() else: incident_id = iris.client.incident(plan_name, context=data) except (ValueError, ConnectionError, HTTPError) as e: raise HTTPBadRequest('Iris escalation failed', 'Iris client error: %s' % e) resp.body = str(incident_id)
def on_post(req, resp, user_name): check_user_auth(user_name, req) data = load_json_body(req) params = set(data.keys()) missing_params = required_params - params if missing_params: raise HTTPBadRequest( 'invalid notification setting', 'missing required parameters: %s' % ', '.join(missing_params)) connection = db.connect() cursor = connection.cursor() cursor.execute('SELECT is_reminder FROM notification_type WHERE name = %s', data['type']) # Validation checks: notification type must exist # only one of time_before and only_if_involved can be defined # reminder notifications must define time_before # other notifications must define only_if_involved if cursor.rowcount != 1: raise HTTPBadRequest( 'invalid notification setting', 'notification type %s does not exist' % data['type']) is_reminder = cursor.fetchone()[0] extra_cols = params & other_params if len(extra_cols) != 1: raise HTTPBadRequest( 'invalid notification setting', 'settings must define exactly one of %s' % other_params) extra_col = next(iter(extra_cols)) if is_reminder and extra_col != 'time_before': raise HTTPBadRequest('invalid notification setting', 'reminder setting must define time_before') elif not is_reminder and extra_col != 'only_if_involved': raise HTTPBadRequest( 'invalid notification setting', 'notification setting must define only_if_involved') roles = data.pop('roles') data['user'] = user_name query = '''INSERT INTO `notification_setting` (`user_id`, `team_id`, `mode_id`, `type_id`, {0}) VALUES ((SELECT `id` FROM `user` WHERE `name`= %(user)s), (SELECT `id` FROM `team` WHERE `name` = %(team)s), (SELECT `id` FROM `contact_mode` WHERE `name` = %(mode)s), (SELECT `id` FROM `notification_type` WHERE `name` = %(type)s), %({0})s)'''.format(extra_col) cursor.execute(query, data) if cursor.rowcount != 1: raise HTTPBadRequest( 'invalid request', 'unable to create notification with provided settings') setting_id = cursor.lastrowid query_vals = ', '.join( ['(%d, (SELECT `id` FROM `role` WHERE `name` = %%s))' % setting_id] * len(roles)) try: cursor.execute( 'INSERT INTO `setting_role`(`setting_id`, `role_id`) VALUES ' + query_vals, roles) except db.IntegrityError: raise HTTPBadRequest('invalid request', 'unable to create notification: invalid roles') connection.commit() cursor.close() connection.close() resp.body = json_dumps({'id': setting_id}) resp.status = HTTP_201
def on_put(req, resp, event_id): """ Update an event by id; anyone can update any event within the team **Example request:** .. sourcecode:: http PUT /api/v0/events/1234 HTTP/1.1 Content-Type: application/json { "start": 1428336000, "end": 1428338000, "user": "******", "role": "secondary" } :statuscode 200: Successful update """ data = load_json_body(req) if 'end' in data and 'start' in data and data['start'] >= data['end']: raise HTTPBadRequest('Invalid event update', 'Event must start before it ends') try: update_cols = ', '.join(update_columns[col] for col in data) except KeyError: raise HTTPBadRequest('Invalid event update', 'Invalid column') connection = db.connect() cursor = connection.cursor(db.DictCursor) try: cursor.execute( '''SELECT `event`.`start`, `event`.`end`, `event`.`user_id`, `event`.`role_id`, `event`.`id`, `event`.`note`, `team`.`name` AS `team`, `role`.`name` AS `role`, `user`.`name` AS `user`, `user`.`full_name`, `event`.`team_id` FROM `event` JOIN `team` ON `event`.`team_id` = `team`.`id` JOIN `role` ON `event`.`role_id` = `role`.`id` JOIN `user` ON `event`.`user_id` = `user`.`id` WHERE `event`.`id`=%s''', event_id) event_data = cursor.fetchone() if not event_data: raise HTTPNotFound() new_event = {} for col in update_columns: new_event[col] = data.get(col, event_data[col]) now = time.time() if event_data['start'] < now - constants.GRACE_PERIOD or data[ 'start'] < now - constants.GRACE_PERIOD: # Make an exception for editing event end times if not (all(event_data[key] == new_event[key] for key in ('role', 'start', 'user')) and data['end'] > now): raise HTTPBadRequest('Invalid event update', 'Editing events in the past not allowed') check_calendar_auth(event_data['team'], req) if not user_in_team_by_name(cursor, new_event['user'], event_data['team']): raise HTTPBadRequest('Invalid event update', 'Event user must be part of the team') update_cols += ', `link_id` = NULL' update = 'UPDATE `event` SET ' + update_cols + (' WHERE `id`=%d' % int(event_id)) cursor.execute(update, data) # create audit log new_event = ', '.join('%s: %s' % (key, data[key]) for key in data) create_audit({ 'old_event': event_data, 'request_body': data }, event_data['team'], EVENT_EDITED, req, cursor) cursor.execute( 'SELECT `user_id`, role_id FROM `event` WHERE `id` = %s', event_data['id']) new_ev_data = cursor.fetchone() context = { 'full_name': event_data['full_name'], 'role': event_data['role'], 'team': event_data['team'], 'new_event': new_event } create_notification(context, event_data['team_id'], {event_data['role_id'], new_ev_data['role_id']}, EVENT_EDITED, {event_data['user_id'], new_ev_data['user_id']}, cursor, start_time=event_data['start']) except: raise else: connection.commit() finally: cursor.close() connection.close()
def validate_post(self, body): if not all(k in body for k in("ruleName", "state", "message")): raise HTTPBadRequest('missing ruleName, state and/or message attributes')
def on_post(req, resp, team): """ Add user as a team admin. Responds with that user's info (similar to user GET). Subscribes this user to default notifications for the team, and adds the user to the team (if needed). **Example request** .. sourcecode:: http POST /api/v0/teams/team-foo/admins HTTP/1.1 Host: example.com **Example response**: .. sourcecode:: http HTTP/1.1 200 OK Content-Type: application/json { "active": 1, "contacts": { "call": "+1 111-111-1111", "email": "*****@*****.**", "im": "jdoe", "sms": "+1 111-111-1111" }, "full_name": "John Doe", "id": 9535, "name": "jdoe", "photo_url": "image.example.com", "time_zone": "US/Pacific" } :statuscode 201: Successful admin added :statuscode 400: Missing name attribute in request :statuscode 422: Invalid team/user, or user is already a team admin """ team = unquote(team) check_team_auth(team, req) data = load_json_body(req) user_name = data.get('name') if not user_name: raise HTTPBadRequest('name attribute missing from request') connection = db.connect() cursor = connection.cursor() cursor.execute('''(SELECT `id` FROM `team` WHERE `name`=%s) UNION ALL (SELECT `id` FROM `user` WHERE `name`=%s)''', (team, user_name)) results = [r[0] for r in cursor] if len(results) < 2: raise HTTPError('422 Unprocessable Entity', 'IntegrityError', 'invalid team or user') (team_id, user_id) = results try: # also make sure user is in the team cursor.execute('''INSERT IGNORE INTO `team_user` (`team_id`, `user_id`) VALUES (%r, %r)''', (team_id, user_id)) cursor.execute('''INSERT INTO `team_admin` (`team_id`, `user_id`) VALUES (%r, %r)''', (team_id, user_id)) # subscribe user to team notifications subscribe_notifications(team, user_name, cursor) create_audit({'user': user_name}, team, ADMIN_CREATED, req, cursor) connection.commit() except db.IntegrityError as e: err_msg = str(e.args[1]) if err_msg == "Column 'team_id' cannot be null": err_msg = 'team %s not found' % team if err_msg == "Column 'user_id' cannot be null": err_msg = 'user %s not found' % data['name'] else: err_msg = 'user name "%s" is already an admin of team %s' % (data['name'], team) raise HTTPError('422 Unprocessable Entity', 'IntegrityError', err_msg) finally: cursor.close() connection.close() resp.status = HTTP_201 resp.body = json_dumps(get_user_data(None, {'name': user_name})[0])
def on_put(req, resp, team, roster): """ Change roster name. Must have team admin privileges. **Example request:** .. sourcecode:: http PUT /api/v0/teams/team-foo/rosters/roster-foo HTTP/1.1 Content-Type: application/json { "name": "roster-bar", } :statuscode 400: Invalid roster name, disallowed characters :statuscode 422: Duplicate roster name for team """ team, roster = unquote(team), unquote(roster) data = load_json_body(req) name = data.get('name') roster_order = data.get('roster_order') check_team_auth(team, req) if not (name or roster_order): raise HTTPBadRequest('invalid roster update', 'missing roster name or order') connection = db.connect() cursor = connection.cursor() try: if roster_order: cursor.execute( '''SELECT `user`.`name` FROM `roster_user` JOIN `roster` ON `roster`.`id` = `roster_user`.`roster_id` JOIN `user` ON `roster_user`.`user_id` = `user`.`id` WHERE `roster_id` = (SELECT id FROM roster WHERE name = %s AND team_id = (SELECT id from team WHERE name = %s))''', (roster, team)) roster_users = {row[0] for row in cursor} if not all([x in roster_users for x in roster_order]): raise HTTPBadRequest( 'Invalid roster order', 'All users in provided order must be part of the roster') if not len(roster_order) == len(roster_users): raise HTTPBadRequest( 'Invalid roster order', 'Roster order must include all roster members') cursor.executemany( '''UPDATE roster_user SET roster_priority = %s WHERE roster_id = (SELECT id FROM roster WHERE name = %s AND team_id = (SELECT id FROM team WHERE name = %s)) AND user_id = (SELECT id FROM user WHERE name = %s)''', ((idx, roster, team, user) for idx, user in enumerate(roster_order))) connection.commit() if name and name != roster: invalid_char = invalid_char_reg.search(name) if invalid_char: raise HTTPBadRequest( 'invalid roster name', 'roster name contains invalid character "%s"' % invalid_char.group()) cursor.execute( '''UPDATE `roster` SET `name`=%s WHERE `team_id`=(SELECT `id` FROM `team` WHERE `name`=%s) AND `name`=%s''', (name, team, roster)) create_audit({ 'old_name': roster, 'new_name': name }, team, ROSTER_EDITED, req, cursor) connection.commit() except db.IntegrityError as e: err_msg = str(e.args[1]) if 'Duplicate entry' in err_msg: err_msg = "roster '%s' already existed for team '%s'" % (name, team) raise HTTPError('422 Unprocessable Entity', 'IntegrityError', err_msg) finally: cursor.close() connection.close()
def on_post(req, resp): ''' Endpoint for team creation. The user who creates the team is automatically added as a team admin. Because of this, this endpoint cannot be called using an API key, otherwise a team would have no admins, making many team operations impossible. Teams can specify a number of attributes, detailed below: - name: the team's name. Teams must have unique names. - email: email address for the team. - slack_channel: slack channel for the team. Must start with '#' - iris_plan: Iris escalation plan that incidents created from the Oncall UI will follow. If iris plan integration is not activated, this attribute can still be set, but its value is not used. Teams must specify ``name`` and ``scheduling_timezone``; other parameters are optional. **Example request:** .. sourcecode:: http POST api/v0/teams HTTP/1.1 Content-Type: application/json { "name": "team-foo", "scheduling_timezone": "US/Pacific", "email": "*****@*****.**", "slack_channel": "#team-foo", } **Example response:** .. sourcecode:: http HTTP/1.1 201 Created Content-Type: application/json :statuscode 201: Successful create :statuscode 400: Error in creating team. Possible errors: API key auth not allowed, invalid attributes, missing required attributes :statuscode 422: Duplicate team name ''' if 'user' not in req.context: # ban API auth because we don't know who to set as team admin raise HTTPBadRequest('invalid login', 'API key auth is not allowed for team creation') data = load_json_body(req) if not data.get('name'): raise HTTPBadRequest('', 'name attribute missing from request') if not data.get('scheduling_timezone'): raise HTTPBadRequest('', 'scheduling_timezone attribute missing from request') team_name = unquote(data['name']) invalid_char = invalid_char_reg.search(team_name) if invalid_char: raise HTTPBadRequest('invalid team name', 'team name contains invalid character "%s"' % invalid_char.group()) scheduling_timezone = unquote(data['scheduling_timezone']) slack = data.get('slack_channel') if slack and slack[0] != '#': raise HTTPBadRequest('invalid slack channel', 'slack channel name needs to start with #') email = data.get('email') iris_plan = data.get('iris_plan') iris_enabled = data.get('iris_enabled', False) override_number = data.get('override_phone_number') if not override_number: override_number = None # validate Iris plan if provided and Iris is configured if iris_plan is not None and iris.client is not None: plan_resp = iris.client.get(iris.client.url + 'plans?name=%s&active=1' % iris_plan) if plan_resp.status_code != 200 or plan_resp.json() == []: raise HTTPBadRequest('invalid iris escalation plan', 'no iris plan named %s exists' % iris_plan) connection = db.connect() cursor = connection.cursor() try: cursor.execute('''INSERT INTO `team` (`name`, `slack_channel`, `email`, `scheduling_timezone`, `iris_plan`, `iris_enabled`, `override_phone_number`) VALUES (%s, %s, %s, %s, %s, %s, %s)''', (team_name, slack, email, scheduling_timezone, iris_plan, iris_enabled, override_number)) team_id = cursor.lastrowid query = ''' INSERT INTO `team_user` (`team_id`, `user_id`) VALUES (%s, (SELECT `id` FROM `user` WHERE `name` = %s))''' cursor.execute(query, (team_id, req.context['user'])) query = ''' INSERT INTO `team_admin` (`team_id`, `user_id`) VALUES (%s, (SELECT `id` FROM `user` WHERE `name` = %s))''' cursor.execute(query, (team_id, req.context['user'])) subscribe_notifications(team_name, req.context['user'], cursor) create_audit({'team_id': team_id}, data['name'], TEAM_CREATED, req, cursor) connection.commit() except db.IntegrityError: raise HTTPError('422 Unprocessable Entity', 'IntegrityError', 'team name "%s" already exists' % team_name) finally: cursor.close() connection.close() resp.status = HTTP_201
def on_post(self, req, resp): ''' This endpoint is compatible with the webhook post from Alertmanager. Simply configure alertmanager with a receiver pointing to iris, like so: receivers: - name: 'iris-team1' webhook_configs: - url: http://iris:16649/v0/webhooks/alertmanager?application=test-app&key=sdffdssdf Where application points to an application and key in Iris. For every POST from alertmanager, a new incident will be created, if the iris_plan label is attached to an alert. ''' alert_params = ujson.loads(req.context['body']) self.validate_post(alert_params) with db.guarded_session() as session: plan = alert_params['groupLabels']['iris_plan'] plan_id = session.execute( 'SELECT `plan_id` FROM `plan_active` WHERE `name` = :plan', { 'plan': plan }).scalar() if not plan_id: raise HTTPNotFound() app = req.context['app'] context_json_str = self.create_context(alert_params) app_template_count = session.execute( ''' SELECT EXISTS ( SELECT 1 FROM `plan_notification` JOIN `template` ON `template`.`name` = `plan_notification`.`template` JOIN `template_content` ON `template_content`.`template_id` = `template`.`id` WHERE `plan_notification`.`plan_id` = :plan_id AND `template_content`.`application_id` = :app_id ) ''', { 'app_id': app['id'], 'plan_id': plan_id }).scalar() if not app_template_count: logger.warning('no plan template exists for this app') raise HTTPBadRequest( 'No plan template actions exist for this app') data = { 'plan_id': plan_id, 'created': datetime.datetime.utcnow(), 'application_id': app['id'], 'context': context_json_str, 'current_step': 0, 'active': True, } incident_id = session.execute( '''INSERT INTO `incident` (`plan_id`, `created`, `context`, `current_step`, `active`, `application_id`) VALUES (:plan_id, :created, :context, 0, :active, :application_id)''', data).lastrowid session.commit() session.close() resp.status = HTTP_201 resp.set_header('Location', '/incidents/%s' % incident_id) resp.body = ujson.dumps(incident_id)
def on_put(req, resp, team): ''' Edit a team's information. Allows edit of: name, slack_channel, email, scheduling_timezone, iris_plan. **Example request:** .. sourcecode:: http PUT /api/v0/teams/team-foo HTTP/1.1 Content-Type: application/json { "name": "team-bar", "slack_channel": "roster-bar", "email": 28, "scheduling_timezone": "US/Central" } :statuscode 200: Successful edit :statuscode 400: Invalid team name/iris escalation plan :statuscode 422: Duplicate team name ''' team = unquote(team) check_team_auth(team, req) data = load_json_body(req) connection = db.connect() cursor = connection.cursor() data_cols = data.keys() if 'name' in data: invalid_char = invalid_char_reg.search(data['name']) if invalid_char: raise HTTPBadRequest( 'invalid team name', 'team name contains invalid character "%s"' % invalid_char.group()) elif data['name'] == '': raise HTTPBadRequest('invalid team name', 'empty team name') if 'iris_plan' in data and data['iris_plan']: iris_plan = data['iris_plan'] plan_resp = iris.client.get(iris.client.url + 'plans?name=%s&active=1' % iris_plan) if plan_resp.status_code != 200 or plan_resp.json() == []: raise HTTPBadRequest('invalid iris escalation plan', 'no iris plan named %s exists' % iris_plan) set_clause = ', '.join( ['`{0}`=%s'.format(d) for d in data_cols if d in cols]) query_params = tuple(data[d] for d in data_cols) + (team, ) try: update_query = 'UPDATE `team` SET {0} WHERE name=%s'.format(set_clause) cursor.execute(update_query, query_params) create_audit({'request_body': data}, team, TEAM_EDITED, req, cursor) connection.commit() except db.IntegrityError as e: err_msg = str(e.args[1]) if 'Duplicate entry' in err_msg: err_msg = "A team named '%s' already exists" % (data['name']) raise HTTPError('422 Unprocessable Entity', 'IntegrityError', err_msg) finally: cursor.close() connection.close()
def on_post(req, resp): """ Endpoint for creating event. Responds with event id for created event. Events must specify the following parameters: - start: Unix timestamp for the event start time (seconds) - end: Unix timestamp for the event end time (seconds) - user: Username for the event's user - team: Name for the event's team - role: Name for the event's role All of these parameters are required. **Example request:** .. sourcecode:: http POST api/v0/events HTTP/1.1 Content-Type: application/json { "start": 1493667700, "end": 149368700, "user": "******", "team": "team-foo", "role": "primary", } **Example response:** .. sourcecode:: http HTTP/1.1 201 Created Content-Type: application/json 1 :statuscode 201: Event created :statuscode 400: Event validation checks failed :statuscode 422: Event creation failed: nonexistent role/event/team """ data = load_json_body(req) now = time.time() if data['start'] < now - constants.GRACE_PERIOD: raise HTTPBadRequest('Invalid event', 'Creating events in the past not allowed') if data['start'] >= data['end']: raise HTTPBadRequest('Invalid event', 'Event must start before it ends') check_calendar_auth(data['team'], req) columns = ['`start`', '`end`', '`user_id`', '`team_id`', '`role_id`'] values = [ '%(start)s', '%(end)s', '(SELECT `id` FROM `user` WHERE `name`=%(user)s)', '(SELECT `id` FROM `team` WHERE `name`=%(team)s)', '(SELECT `id` FROM `role` WHERE `name`=%(role)s)' ] if 'schedule_id' in data: columns.append('`schedule_id`') values.append('%(schedule_id)s') if 'note' in data: columns.append('`note`') values.append('%(note)s') connection = db.connect() cursor = connection.cursor(db.DictCursor) if not user_in_team_by_name(cursor, data['user'], data['team']): raise HTTPBadRequest('Invalid event', 'User must be part of the team') try: query = 'INSERT INTO `event` (%s) VALUES (%s)' % (','.join(columns), ','.join(values)) cursor.execute(query, data) event_id = cursor.lastrowid cursor.execute( 'SELECT team_id, role_id, user_id, start, full_name ' 'FROM event JOIN user ON user.`id` = user_id WHERE event.id=%s', event_id) ev_info = cursor.fetchone() context = { 'team': data['team'], 'role': data['role'], 'full_name': ev_info['full_name'] } create_notification(context, ev_info['team_id'], [ev_info['role_id']], EVENT_CREATED, [ev_info['user_id']], cursor, start_time=ev_info['start']) create_audit({ 'new_event_id': event_id, 'request_body': data }, data['team'], EVENT_CREATED, req, cursor) connection.commit() except db.IntegrityError as e: err_msg = str(e.args[1]) if err_msg == 'Column \'role_id\' cannot be null': err_msg = 'role "%s" not found' % data['role'] elif err_msg == 'Column \'user_id\' cannot be null': err_msg = 'user "%s" not found' % data['user'] elif err_msg == 'Column \'team_id\' cannot be null': err_msg = 'team "%s" not found' % data['team'] raise HTTPError('422 Unprocessable Entity', 'IntegrityError', err_msg) finally: cursor.close() connection.close() resp.status = HTTP_201 resp.body = json_dumps(event_id)
def on_put(req, resp, link_id): """ Update an event by link_id; anyone can update any event within the team. Only username can be updated using this endpoint. **Example request:** .. sourcecode:: http PUT /api/v0/events/link/1234 HTTP/1.1 Content-Type: application/json { "user": "******", } :statuscode 200: Successful update """ data = load_json_body(req) user = data.get('user') if user is None: raise HTTPBadRequest('Bad request for linked event update', 'Missing user param') connection = db.connect() cursor = connection.cursor(db.DictCursor) try: cursor.execute( '''SELECT `event`.`start`, `event`.`end`, `event`.`user_id`, `event`.`role_id`, `event`.`id`, `team`.`name` AS `team`, `role`.`name` AS `role`, `user`.`name` AS `user`, `user`.`full_name`, `event`.`team_id` FROM `event` JOIN `team` ON `event`.`team_id` = `team`.`id` JOIN `role` ON `event`.`role_id` = `role`.`id` JOIN `user` ON `event`.`user_id` = `user`.`id` WHERE `event`.`link_id`=%s''', link_id) event_data = cursor.fetchall() if len(event_data) == 0: raise HTTPNotFound() event_summary = event_data[0].copy() event_summary['end'] = max(event_data, key=itemgetter('end'))['end'] event_summary['start'] = min(event_data, key=itemgetter('start'))['start'] if not user_in_team_by_name(cursor, user, event_summary['team']): raise HTTPBadRequest('Invalid event update', 'Event user must be part of the team') now = time.time() if event_summary['end'] < now: raise HTTPBadRequest('Invalid event update', 'Editing events in the past not allowed') check_calendar_auth(event_summary['team'], req) cursor.execute('SELECT `id` FROM `user` WHERE `name` = %s', user) if cursor.rowcount == 0: raise HTTPBadRequest('Invalid event update', 'No user found with specified name') user_id = cursor.fetchone()['id'] cursor.execute( '''UPDATE `event` SET `user_id` = %s WHERE link_id = %s''', (user_id, link_id)) create_audit({ 'old_event': event_summary, 'request_body': data }, event_summary['team'], EVENT_EDITED, req, cursor) context = { 'full_name': event_summary['full_name'], 'role': event_summary['role'], 'team': event_summary['team'], 'new_event': { 'user': user } } create_notification(context, event_summary['team_id'], {event_summary['role_id']}, EVENT_EDITED, {event_summary['user_id'], user_id}, cursor, start_time=event_summary['start']) connection.commit() finally: cursor.close() connection.close() resp.status = HTTP_204
def on_post(req, resp, team, roster): ''' Schedule create endpoint. Schedules are templates for the auto-scheduler to follow that define how it should populate a certain period of time. This template is followed repeatedly to populate events on a team's calendar. Schedules are associated with a roster, which defines the pool of users that the scheduler selects from. Similarly, the schedule's role indicates the role that the populated events shoud have. The ``auto_populate_threshold`` parameter defines how far into the future the scheduler populates. Finally, each schedule has a list of events, each defining ``start`` and ``duration``. ``start`` represents an offset from Sunday at 00:00 in the team's scheduling timezone, in seconds. For example, denote DAY and HOUR as the number of seconds in a day/hour, respectively. An event with ``start`` of (DAY + 9 * HOUR) starts on Monday, at 9:00 am. Duration is also given in seconds. The scheduler will start at Sunday 00:00 in the team's scheduling timezone, choose a user, and populate events on the calendar according to the offsets defined in the events list. It then repeats this process, moving to the next Sunday 00:00 after the events it has created. ``advanced_mode`` acts as a hint to the frontend on how the schedule should be displayed, defining whether the advanced mode toggle on the schedule edit action should be set on or off. Because of how the frontend displays simple schedules, a schedule can only have advanced_mode = 0 if its events have one of 4 formats: 1. One event that is one week long 2. One event that is two weeks long 3. Seven events that are 12 hours long 4. Fourteen events that are 12 hours long See below for sample JSON requests. Assume these schedules' team defines US/Pacific as its scheduling timezone. Weekly 7*24 shift that starts at Monday 6PM PST: .. code-block:: javascript { 'role': 'primary' 'auto_populate_threshold': 21, 'events':[ {'start': SECONDS_IN_A_DAY + 18 * SECONDS_IN_AN_HOUR, 'duration': SECONDS_IN_A_WEEK} ], 'advanced_mode': 0 } Weekly 7*12 shift that starts at Monday 8AM PST: .. code-block:: javascript { 'role': 'oncall', 'events':[ {'start': SECONDS_IN_A_DAY + 8 * SECONDS_IN_AN_HOUR, 'duration': 12 * SECONDS_IN_AN_HOUR}, {'start': 2 * SECONDS_IN_A_DAY + 8 * SECONDS_IN_AN_HOUR, 'duration': 12 * SECONDS_IN_AN_HOUR} ... *5 more* ], 'advanced_mode': 1 } **Example Request** .. sourcecode:: http POST /v0/teams/team-foo/rosters/roster-foo/schedules HTTP/1.1 Content-Type: application/json { "advanced_mode": 0, "auto_populate_threshold": "21", "events": [ { "duration": 604800, "start": 129600 } ], "role": "primary", } **Example response**: .. sourcecode:: http HTTP/1.1 201 OK Content-Type: application/json { "id": 2221 } :statuscode 201: Successful schedule create. Response contains created schedule's id. :statuscode 400: Missing required parameters :statuscode 422: Invalid roster specified ''' data = load_json_body(req) data['team'] = unquote(team) data['roster'] = unquote(roster) check_team_auth(data['team'], req) missing_params = required_params - set(data.keys()) if missing_params: raise HTTPBadRequest('invalid schedule', 'missing required parameters: %s' % ', '.join(missing_params)) schedule_events = data.pop('events') for sev in schedule_events: if 'start' not in sev or 'duration' not in sev: raise HTTPBadRequest('invalid schedule', 'schedule event requires both start and duration fields') if 'auto_populate_threshold' not in data: # default to autopopulate 3 weeks forward data['auto_populate_threshold'] = 21 if 'scheduler' not in data: # default to "default" scheduling algorithm data['scheduler_name'] = 'default' else: data['scheduler_name'] = data['scheduler'].get('name', 'default') scheduler_data = data['scheduler'].get('data') if not data['advanced_mode']: if not validate_simple_schedule(schedule_events): raise HTTPBadRequest('invalid schedule', 'invalid advanced mode setting') insert_schedule = '''INSERT INTO `schedule` (`roster_id`,`team_id`,`role_id`, `auto_populate_threshold`, `advanced_mode`, `scheduler_id`) VALUES ((SELECT `roster`.`id` FROM `roster` JOIN `team` ON `roster`.`team_id` = `team`.`id` WHERE `roster`.`name` = %(roster)s AND `team`.`name` = %(team)s), (SELECT `id` FROM `team` WHERE `name` = %(team)s), (SELECT `id` FROM `role` WHERE `name` = %(role)s), %(auto_populate_threshold)s, %(advanced_mode)s, (SELECT `id` FROM `scheduler` WHERE `name` = %(scheduler_name)s))''' connection = db.connect() cursor = connection.cursor(db.DictCursor) try: cursor.execute(insert_schedule, data) schedule_id = cursor.lastrowid insert_schedule_events(schedule_id, schedule_events, cursor) if data['scheduler_name'] == 'round-robin': params = [(schedule_id, name, idx) for idx, name in enumerate(scheduler_data)] cursor.executemany('''INSERT INTO `schedule_order` (`schedule_id`, `user_id`, `priority`) VALUES (%s, (SELECT `id` FROM `user` WHERE `name` = %s), %s)''', params) except db.IntegrityError as e: err_msg = str(e.args[1]) if err_msg == 'Column \'roster_id\' cannot be null': err_msg = 'roster "%s" not found' % roster elif err_msg == 'Column \'role_id\' cannot be null': err_msg = 'role not found' elif err_msg == 'Column \'scheduler_id\' cannot be null': err_msg = 'scheduler not found' elif err_msg == 'Column \'team_id\' cannot be null': err_msg = 'team "%s" not found' % team raise HTTPError('422 Unprocessable Entity', 'IntegrityError', err_msg) else: connection.commit() finally: cursor.close() connection.close() resp.status = HTTP_201 resp.body = json_dumps({'id': schedule_id})
def as_bad_request(self): """Translate this error to falcon's HTTP specific error exception.""" return HTTPBadRequest(title="Representation deserialization failed", description=self._get_description())
def on_post(req, resp, team, roster): """ Add user to a roster for a team. On successful creation, returns that user's information. This includes id, contacts, etc, similar to the /api/v0/users GET endpoint. **Example request:** .. sourcecode:: http POST /v0/teams/team-foo/rosters/roster-foo/users HTTP/1.1 Content-Type: application/json { "name": "jdoe" } **Example response:** .. sourcecode:: http HTTP/1.1 200 OK Content-Type: application/json { "active": 1, "contacts": { "email": "*****@*****.**", "im": "jdoe", "sms": "+1 111-111-1111", "call": "+1 111-111-1111" }, "full_name": "John Doe", "id": 1, "name": "jdoe", "photo_url": "example.image.com", "time_zone": "US/Pacific" } :statuscode 201: Roster user added :statuscode 400: Missing "name" parameter :statuscode 422: Invalid team/user or user is already in roster. """ team, roster = unquote(team), unquote(roster) data = load_json_body(req) user_name = data.get('name') in_rotation = int(data.get('in_rotation', True)) if not user_name: raise HTTPBadRequest('incomplete data', 'missing field "name"') check_team_auth(team, req) connection = db.connect() cursor = connection.cursor() cursor.execute('''(SELECT `id` FROM `team` WHERE `name`=%s) UNION ALL (SELECT `id` FROM `user` WHERE `name`=%s)''', (team, user_name)) results = [r[0] for r in cursor] if len(results) < 2: raise HTTPError('422 Unprocessable Entity', 'IntegrityError', 'invalid team or user') # TODO: validate roster (team_id, user_id) = results try: # also make sure user is in the team cursor.execute('''INSERT IGNORE INTO `team_user` (`team_id`, `user_id`) VALUES (%r, %r)''', (team_id, user_id)) cursor.execute('''SELECT `roster`.`id`, COALESCE(MAX(`roster_user`.`roster_priority`), -1) + 1 FROM `roster` LEFT JOIN `roster_user` ON `roster`.`id` = `roster_id` JOIN `team` ON `team`.`id`=`roster`.`team_id` WHERE `team`.`name`=%s AND `roster`.`name`=%s''', (team, roster)) if cursor.rowcount == 0: raise HTTPNotFound() roster_id, roster_priority = cursor.fetchone() cursor.execute('''INSERT INTO `roster_user` (`user_id`, `roster_id`, `in_rotation`, `roster_priority`) VALUES ( %s, %s, %s, %s )''', (user_id, roster_id, in_rotation, roster_priority)) cursor.execute('''INSERT INTO `schedule_order` SELECT `schedule_id`, %s, COALESCE(MAX(`schedule_order`.`priority`), -1) + 1 FROM `schedule_order` JOIN `schedule` ON `schedule`.`id` = `schedule_order`.`schedule_id` JOIN `roster` ON `roster`.`id` = `schedule`.`roster_id` JOIN `team` ON `roster`.`team_id` = `team`.`id` WHERE `roster`.`name` = %s AND `team`.`name` = %s GROUP BY `schedule_id`''', (user_id, roster, team)) # subscribe user to notifications subscribe_notifications(team, user_name, cursor) create_audit({'roster': roster, 'user': user_name, 'request_body': data}, team, ROSTER_USER_ADDED, req, cursor) connection.commit() except db.IntegrityError: raise HTTPError('422 Unprocessable Entity', 'IntegrityError', 'user "%(name)s" is already in the roster' % data) finally: cursor.close() connection.close() resp.status = HTTP_201 resp.body = json_dumps(get_user_data(None, {'name': user_name})[0])
def on_post(self, req, resp, plan): ''' For every POST, a new incident will be created, if the plan label is attached to an alert. The iris application and key should be provided in the url params. The plan id can be taken from the post body or url params passed by the webhook subclass. ''' alert_params = ujson.loads(req.context['body']) self.validate_post(alert_params) with db.guarded_session() as session: plan_id = session.execute( 'SELECT `plan_id` FROM `plan_active` WHERE `name` = :plan', { 'plan': plan }).scalar() if not plan_id: raise HTTPNotFound() app = req.context['app'] context_json_str = self.create_context(alert_params) app_template_count = session.execute( ''' SELECT EXISTS ( SELECT 1 FROM `plan_notification` JOIN `template` ON `template`.`name` = `plan_notification`.`template` JOIN `template_content` ON `template_content`.`template_id` = `template`.`id` WHERE `plan_notification`.`plan_id` = :plan_id AND `template_content`.`application_id` = :app_id ) ''', { 'app_id': app['id'], 'plan_id': plan_id }).scalar() if not app_template_count: logger.warn('no plan template exists for this app') raise HTTPBadRequest( 'No plan template actions exist for this app') data = { 'plan_id': plan_id, 'created': datetime.datetime.utcnow(), 'application_id': app['id'], 'context': context_json_str, 'current_step': 0, 'active': True, } incident_id = session.execute( '''INSERT INTO `incident` (`plan_id`, `created`, `context`, `current_step`, `active`, `application_id`) VALUES (:plan_id, :created, :context, 0, :active, :application_id)''', data).lastrowid session.commit() session.close() resp.status = HTTP_201 resp.set_header('Location', '/incidents/%s' % incident_id) resp.body = ujson.dumps(incident_id)
def on_post(req, resp, team): """ Create a roster for a team **Example request:** .. sourcecode:: http POST /v0/teams/team-foo/rosters HTTP/1.1 Content-Type: application/json { "name": "roster-foo", } **Example response:** .. sourcecode:: http HTTP/1.1 201 Created Content-Type: application/json :statuscode 201: Succesful roster creation :statuscode 422: Invalid character in roster name/Duplicate roster name """ team = unquote(team) data = load_json_body(req) roster_name = data.get('name') if not roster_name: raise HTTPBadRequest('name attribute missing from request', '') invalid_char = invalid_char_reg.search(roster_name) if invalid_char: raise HTTPBadRequest( 'invalid roster name', 'roster name contains invalid character "%s"' % invalid_char.group()) check_team_auth(team, req) connection = db.connect() cursor = connection.cursor() try: cursor.execute( '''INSERT INTO `roster` (`name`, `team_id`) VALUES (%s, (SELECT `id` FROM `team` WHERE `name`=%s))''', (roster_name, team)) except db.IntegrityError: raise HTTPError( '422 Unprocessable Entity', 'IntegrityError', 'roster name "%s" already exists for team %s' % (roster_name, team)) create_audit({ 'roster_id': cursor.lastrowid, 'request_body': data }, team, ROSTER_CREATED, req, cursor) connection.commit() cursor.close() connection.close() resp.status = HTTP_201
def on_post(req, resp): """ Override/substitute existing events. For example, if the current on-call is unexpectedly busy from 3-4, another user can override that event for that time period and take over the shift. Override may delete or edit existing events, and may create new events. The API's response contains the information for all undeleted events that were passed in the event_ids param, along with the events created by the override. Params: - **start**: Start time for the event substitution - **end**: End time for event substitution - **event_ids**: List of event ids to override - **user**: User who will be taking over **Example request:** .. sourcecode:: http POST api/v0/events/override HTTP/1.1 Content-Type: application/json { "start": 1493677400, "end": 1493678400, "event_ids": [1], "user": "******" } **Example response:** .. sourcecode:: http HTTP/1.1 200 OK Content-Type: application/json [ { "end": 1493678400, "full_name": "John Doe", "id": 3, "role": "primary", "start": 1493677400, "team": "team-foo", "user": "******" } ] """ data = load_json_body(req) event_ids = data['event_ids'] start = data['start'] end = data['end'] user = data['user'] get_events_query = '''SELECT `start`, `end`, `id`, `schedule_id`, `user_id`, `role_id`, `team_id` FROM `event` WHERE `id` IN %s''' insert_event_query = 'INSERT INTO `event`(`start`, `end`, `user_id`, `team_id`, `role_id`)' \ 'VALUES (%(start)s, %(end)s, %(user_id)s, %(team_id)s, %(role_id)s)' event_return_query = '''SELECT `event`.`start`, `event`.`end`, `event`.`id`, `role`.`name` AS `role`, `team`.`name` AS `team`, `user`.`name` AS `user`, `user`.`full_name` FROM `event` JOIN `role` ON `event`.`role_id` = `role`.`id` JOIN `team` ON `event`.`team_id` = `team`.`id` JOIN `user` ON `event`.`user_id` = `user`.`id` WHERE `event`.`id` IN %s''' connection = db.connect() cursor = connection.cursor(db.DictCursor) try: cursor.execute(get_events_query, (event_ids,)) events = cursor.fetchall() now = time.time() cursor.execute('SELECT `id` FROM `user` WHERE `name` = %s', user) user_id = cursor.fetchone() if not (events and user_id): raise HTTPBadRequest('Invalid name or list of events') else: user_id = user_id['id'] team_id = events[0]['team_id'] check_calendar_auth_by_id(team_id, req) # Check that events are not in the past if start < now - constants.GRACE_PERIOD: raise HTTPBadRequest('Invalid override request', 'Cannot edit events in the past') # Check that events are from the same team if any([ev['team_id'] != team_id for ev in events]): raise HTTPBadRequest('Invalid override request', 'Events must be from the same team') # Check override user's membership in the team if not user_in_team(cursor, user_id, team_id): raise HTTPBadRequest('Invalid override request', 'Substituting user must be part of the team') # Check events have the same role if len(set([ev['role_id'] for ev in events])) > 1: raise HTTPBadRequest('Invalid override request', 'events must have the same role') # Check events have same user if len(set([ev['user_id'] for ev in events])) > 1: raise HTTPBadRequest('Invalid override request', 'events must have the same role') edit_start = [] edit_end = [] delete = [] split = [] events = sorted(events, key=lambda x: x['start']) # Truncate start/end if needed start = max(events[0]['start'], start) end = min(max(e['end'] for e in events), end) for idx, e in enumerate(events): # Check for consecutive events if idx != 0 and e['start'] != events[idx - 1]['end']: raise HTTPBadRequest('Invalid override request', 'events must be consecutive') # Sort events into lists according to how they need to be edited if start <= e['start'] and end >= e['end']: delete.append(e) elif start > e['start'] and start < e['end'] <= end: edit_end.append(e) elif start <= e['start'] < end and end < e['end']: edit_start.append(e) elif start > e['start'] and end < e['end']: split.append(e) else: raise HTTPBadRequest('Invalid override request', 'events must overlap with override time range') # Edit events if edit_start: ids = [e['id'] for e in edit_start] cursor.execute('UPDATE `event` SET `start` = %s WHERE `id` IN %s', (end, ids)) if edit_end: ids = [e['id'] for e in edit_end] cursor.execute('UPDATE `event` SET `end` = %s WHERE `id` IN %s', (start, ids)) if delete: ids = [e['id'] for e in delete] cursor.execute('DELETE FROM `event` WHERE `id` IN %s', (ids,)) if split: create = [] for e in split: left_event = e.copy() right_event = e.copy() left_event['end'] = start right_event['start'] = end create.append(left_event) create.append(right_event) ids = [] # Create left/right events for e in create: cursor.execute(insert_event_query, e) ids.append(cursor.lastrowid) event_ids.append(cursor.lastrowid) # Delete the split event ids = [e['id'] for e in split] cursor.execute('DELETE FROM `event` WHERE `id` IN %s', (ids,)) # Insert new override event override_event = { 'start': start, 'end': end, 'role_id': events[0]['role_id'], 'team_id': events[0]['team_id'], 'user_id': user_id } cursor.execute('''INSERT INTO `event`(`start`, `end`, `user_id`, `team_id`, `role_id`) VALUES (%(start)s, %(end)s, %(user_id)s, %(team_id)s, %(role_id)s)''', override_event) event_ids.append(cursor.lastrowid) cursor.execute(event_return_query, (event_ids,)) ret_data = cursor.fetchall() cursor.execute('SELECT full_name, id FROM user WHERE id IN %s', ((user_id, events[0]['user_id']),)) full_names = {row['id']: row['full_name'] for row in cursor} context = {'full_name_0': full_names[user_id], 'full_name_1': full_names[events[0]['user_id']], 'role': ret_data[0]['role'], 'team': ret_data[0]['team']} create_notification(context, events[0]['team_id'], [events[0]['role_id']], EVENT_SUBSTITUTED, [user_id, events[0]['user_id']], cursor, start_time=start, end_time=end) create_audit({'new_events': ret_data, 'request_body': data}, ret_data[0]['team'], EVENT_SUBSTITUTED, req, cursor) resp.body = json_dumps(ret_data) except HTTPError: raise else: connection.commit() finally: cursor.close() connection.close()
def on_put(req, resp, schedule_id): """ Update a schedule. Allows editing of role, team, roster, auto_populate_threshold, events, and advanced_mode. Only allowed for team admins. Note that simple mode schedules must conform to simple schedule restrictions (described in documentation for the /api/v0/team/{team_name}/rosters/{roster_name}/schedules GET endpoint). This is checked on both "events" and "advanced_mode" edits. **Example request:** .. sourcecode:: http PUT /api/v0/schedules/1234 HTTP/1.1 Content-Type: application/json { "role": "primary", "team": "team-bar", "roster": "roster-bar", "auto_populate_threshold": 28, "events": [ { "start": 0, "duration": 100 } ] "advanced_mode": 1 } """ data = load_json_body(req) # Get rid of extraneous column data (so pymysql doesn't try to escape it) events = data.pop('events', None) data = dict((k, data[k]) for k in data if k in columns) if 'roster' in data and 'team' not in data: raise HTTPBadRequest('Invalid edit', 'team must be specified with roster') cols = ', '.join(columns[col] for col in data) update = 'UPDATE `schedule` SET ' + cols + ' WHERE `id`=%d' % int(schedule_id) connection = db.connect() cursor = connection.cursor() verify_auth(req, schedule_id, connection, cursor) # Validate simple schedule events if events: simple = validate_simple_schedule(events) else: cursor.execute('SELECT duration FROM schedule_event WHERE schedule_id = %s', schedule_id) existing_events = [{'duration': row[0]} for row in cursor.fetchall()] simple = validate_simple_schedule(existing_events) # Get advanced mode value (existing or new) advanced_mode = data.get('advanced_mode') if advanced_mode is None: cursor.execute('SELECT advanced_mode FROM schedule WHERE id = %s', schedule_id) advanced_mode = cursor.fetchone()[0] # if advanced mode is 0 and the events cannot exist as a simple schedule, raise an error if not advanced_mode and not simple: raise HTTPBadRequest('Invalid edit', 'schedule cannot be represented in simple mode') if cols: cursor.execute(update, data) if events: cursor.execute('DELETE FROM `schedule_event` WHERE `schedule_id` = %s', schedule_id) insert_schedule_events(schedule_id, events, cursor) connection.commit() cursor.close() connection.close()
def on_get(req, resp): """ Search for events. Allows filtering based on a number of parameters, detailed below. **Example request**: .. sourcecode:: http GET /api/v0/events?team=foo-sre&end__gt=1487466146&role=primary HTTP/1.1 Host: example.com **Example response**: .. sourcecode:: http HTTP/1.1 200 OK Content-Type: application/json [ { "start": 1488441600, "end": 1489132800, "team": "foo-sre", "link_id": null, "schedule_id": null, "role": "primary", "user": "******", "full_name": "Foo Icecream", "id": 187795 }, { "start": 1488441600, "end": 1489132800, "team": "foo-sre", "link_id": "8a8ae77b8c52448db60c8a701e7bffc2", "schedule_id": 123, "role": "primary", "user": "******", "full_name": "Bar Apple", "id": 187795 } ] :query team: team name :query user: user name :query role: role name :query id: id of the event :query start: start time (unix timestamp) of event :query end: end time (unix timestamp) of event :query start__gt: start time (unix timestamp) greater than :query start__ge: start time (unix timestamp) greater than or equal :query start__lt: start time (unix timestamp) less than :query start__le: start time (unix timestamp) less than or equal :query end__gt: end time (unix timestamp) greater than :query end__ge: end time (unix timestamp) greater than or equal :query end__lt: end time (unix timestamp) less than :query end__le: end time (unix timestamp) less than or equal :query role__eq: role name :query role__contains: role name contains param :query role__startswith: role name starts with param :query role__endswith: role name ends with param :query team__eq: team name :query team__contains: team name contains param :query team__startswith: team name starts with param :query team__endswith: team name ends with param :query team_id: team id :query user__eq: user name :query user__contains: user name contains param :query user__startswith: user name starts with param :query user__endswith: user name ends with param :statuscode 200: no error :statuscode 400: bad request """ fields = req.get_param_as_list('fields', transform=columns.__getitem__) req.params.pop('fields', None) cols = ', '.join(fields) if fields else all_columns if any(key not in constraints for key in req.params): raise HTTPBadRequest('Bad constraint param') query = '''SELECT %s FROM `event` JOIN `user` ON `user`.`id` = `event`.`user_id` JOIN `team` ON `team`.`id` = `event`.`team_id` JOIN `role` ON `role`.`id` = `event`.`role_id`''' % cols where_params = [] where_vals = [] for key in req.params: val = req.get_param(key) if key in constraints: where_params.append(constraints[key]) where_vals.append(val) where_query = ' AND '.join(where_params) if where_query: query = '%s WHERE %s' % (query, where_query) connection = db.connect() cursor = connection.cursor(db.DictCursor) cursor.execute(query, where_vals) data = cursor.fetchall() cursor.close() connection.close() resp.body = json_dumps(data)