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()['id'] team_id = events[0]['team_id'] check_calendar_auth_by_id(team_id, req) # Check that events are not in the past if any([ev['start'] < now - constants.GRACE_PERIOD for ev in events]): 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 process_resource(self, req, resp, resource, params): template = req.uri_template method = req.method if 'auth_type' in params and params['auth_type'] == 'login': pass else: if template in AUTHENTICATION_VALIDATION_PATHS and method in AUTHENTICATION_VALIDATION_PATHS[ template]: auth = req.get_header('Authorization') if auth and auth is not None: try: redis_cli = Redis.get_redis_client() auth_info = redis_cli.hget('USERS_APIKEY', auth) if auth_info is not None: auth_info = json.loads(auth_info) if auth_info['is_active']: if template in ACCESS_TOKEN_VALIDATION_PATHS and method in ACCESS_TOKEN_VALIDATION_PATHS[ template]: access_token = req.get_param( 'access_token') if access_token and access_token is not None: if auth_info[ 'access_token'] == access_token: if not redis_cli.exists( auth_info['access_token']): raise HTTPUnauthorized( description= 'Access token is expired. Please login again and continue' ) else: raise HTTPForbidden( description= 'Access token is not valid.') else: raise HTTPBadRequest( description= 'Access token is not valid.') else: raise HTTPUnauthorized( description= 'access_token is mandatory to acccess the api.' ) else: raise HTTPBadRequest( description='Client is not active.') else: raise HTTPUnauthorized( description= 'Token sent in Authroization header is not valid.' ) except (HTTPUnauthorized, HTTPBadRequest) as err: raise err except Exception as err: print( 'Exception while accessing redis client instance', err) raise HTTPInternalServerError( description='Something went wrong in server') else: raise HTTPUnauthorized( description= 'Authorization header is mandatory to process the request.' )
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 on_put(req, resp, user_name): """ Update user info. Allows edits to: - contacts - name - full_name - time_zone - photo_url - active Takes an object specifying the new values of these attributes. ``contacts`` acts slightly differently, specifying an object with the contact mode as key and new values for that contact mode as values. Any contact mode not specified will be unchanged. Similarly, any field not specified in the PUT will be unchanged. **Example request:** .. sourcecode:: http PUT /api/v0/users/jdoe HTTP/1.1 Content-Type: application/json { "contacts": { "call": "+1 222-222-2222", "email": "*****@*****.**" } "name": "johndoe", "full_name": "Johnathan Doe", } :statuscode 204: Successful edit :statuscode 404: User not found """ contacts_query = '''REPLACE INTO user_contact (`user_id`, `mode_id`, `destination`) VALUES ((SELECT `id` FROM `user` WHERE `name` = %(user)s), (SELECT `id` FROM `contact_mode` WHERE `name` = %(mode)s), %(destination)s) ''' check_user_auth(user_name, req) data = load_json_body(req) set_contacts = False set_columns = [] for field in data: if field == 'contacts': set_contacts = True elif field in writable_columns: set_columns.append('`{0}` = %s'.format(field)) set_clause = ', '.join(set_columns) connection = db.connect() cursor = connection.cursor() if set_clause: query = 'UPDATE `user` SET {0} WHERE `name` = %s'.format(set_clause) query_data = [] for field in data: if field != 'contacts': query_data.append(data[field]) query_data.append(user_name) cursor.execute(query, query_data) if cursor.rowcount != 1: cursor.close() connection.close() raise HTTPBadRequest('No User Found', 'no user exists with given name') if set_contacts: contacts = [] for mode, dest in data['contacts'].iteritems(): contact = {} contact['mode'] = mode contact['destination'] = dest contact['user'] = user_name contacts.append(contact) cursor.executemany(contacts_query, contacts) connection.commit() cursor.close() connection.close() resp.status = HTTP_204
def process_resource_inner(self, req, resp, resource, params): # type: (Request, Response, object, dict) -> None """Deserialize request body with any resource-specific schemas Store deserialized data on the ``req.context`` object under the ``req_key`` provided to the class constructor or on the ``json`` key if none was provided. If a Marshmallow schema is defined on the passed ``resource``, use it to deserialize the request body. If no schema is defined and the class was instantiated with ``force_json=True``, request data will be deserialized with any ``json_module`` passed to the class constructor or ``simplejson`` by default. :param falcon.Request req: the request object :param falcon.Response resp: the response object :param object resource: the resource object :param dict params: any parameters parsed from the url :rtype: None :raises falcon.HTTPBadRequest: if the data cannot be deserialized or decoded """ log.debug('Marshmallow.process_resource(%s, %s, %s, %s)', req, resp, resource, params) if req.content_length in (None, 0): return sch = self._get_schema(resource, req.method, 'request') if sch is not None: if not isinstance(sch, Schema): raise TypeError( 'The schema and <method>_schema properties of a resource ' 'must be instantiated Marshmallow schemas.') try: body = get_stashed_content(req) parsed = self._json.loads(body) except UnicodeDecodeError: raise HTTPBadRequest('Body was not encoded as UTF-8') except self._json.JSONDecodeError: raise HTTPBadRequest('Request must be valid JSON') log.info(sch) data = sch.load(parsed) req.context[self._req_key] = data elif self._force_json: body = get_stashed_content(req) try: req.context[self._req_key] = self._json.loads(body) except (ValueError, UnicodeDecodeError): raise HTTPBadRequest(description=( 'Could not decode the request body, either because ' 'it was not valid JSON or because it was not encoded ' 'as UTF-8.'))
def on_post(req, resp, user_name): ''' Endpoint to create notification settings for a user. Responds with an object denoting the created setting's id. Requests to create notification settings must define the following: - team - roles - mode - type Users will be notified via ``$mode`` if a ``$type`` action occurs on the ``$team`` calendar that modifies events having a role contained in ``$roles``. In addition to these parameters, notification settings must define one of ``time_before`` and ``only_if_involved``, depending on whether the notification type is a reminder or a notification. Reminders define a ``time_before`` and reference the start/end time of an event that user is involved in. There are two reminder types: "oncall_reminder" and "offcall_reminder", referencing the start and end of on-call events, respectively. ``time_before`` is specified in seconds and denotes how far in advance the user should be reminded of an event. Notifications are event-driven, and created when a team's calendar is modified. By default, the notification types are: - event_created - event_edited - event_deleted - event_swapped - event_substituted Non-reminder settings must define ``only_if_involved`` which determines whether the user will be notified on all actions of the given typ or only on ones in which they are involved. Note that ``time_before`` must not be specified for a non-reminder setting, and ``only_if_involved`` must not be specified for reminder settings. An authoritative list of notification types can be obtained from the /api/v0/notification_types GET endpoint, which also details whether the type is a reminder. This will obtain all notification type data from the database, and is an absolute source of truth for Oncall. **Example request:** .. sourcecode:: http POST api/v0/events HTTP/1.1 Content-Type: application/json { "team": "team-foo", "roles": ["primary", "secondary"], "mode": "email", "type": "event_created", "only_if_involved": true } **Example response:** .. sourcecode:: http HTTP/1.1 201 Created Content-Type: application/json { "id": 1234 } ''' 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, notification_id): 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 wrapper(self, req, resp, *args, **kwargs): try: req.context["json"] = json.load(req.bounded_stream) except json.JSONDecodeError: raise HTTPBadRequest(description="failed to decode JSON payload") return h(self, req, resp, *args, **kwargs)
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): ''' 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.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 create_image_encodings(body): required = ('images', 'dets_list', 'filetypes', 'image_names', 'training') if not all([r in body.keys() for r in required]): raise HTTPBadRequest(description="Required:" + ", ".join(required)) images = body['images'] dets_list = body['dets_list'] image_names = body['image_names'] training = body['training'] img_lst = [] global embed_total if training == 'True': embedding_file = PATH_TRAINING_EMBEDDING_FILE embedding_label_file = PATH_TRAINING_EMBEDDING_LABEL_FILE else: embedding_file = PATH_EMBEDDING_FILE embedding_label_file = PATH_EMBEDDING_LABEL_FILE if 'total_images' in body.keys(): embed_total = int(body['total_images']) if os.path.exists(embedding_file): os.remove(embedding_file) if os.path.exists(embedding_label_file): os.remove(embedding_label_file) h5_file = h5py.File(embedding_file, "a") features = h5_file.create_dataset('encodings', shape=(0, 512), dtype='float32', maxshape=(None, 512)) else: features = load_h5py_dataset(training) if type(images) == str: images = [images] for id, (image, filetype) in enumerate(zip(images, body['filetypes'])): try: img_lst.append(stringToRGB(image)) except: img_lst.append('') raise CouldNotReadFileError() try: fa = init_aligner() except: raise AlignerNotFoundError() try: # https://github.com/davidsandberg/facenet/issues/1112 # Use pb file instead of meta and ckpt with tf2+ encoder = Encoder(PATH_MODEL_FILE) except: raise ModelNotFoundError() with open(embedding_label_file, 'a') as f: writer = csv.writer(f) errors = [] face_count = features.shape[0] for img, image_name, dets in zip(img_lst, image_names, dets_list): if img is None: continue error = '' dets = list(dets.split(",")) try: imcrops = align_faces(img, dets, fa) for i, imcrop in enumerate(imcrops): try: embedding = embed_face(imcrop, encoder=encoder) features.resize(features.shape[0] + 1, axis=0) features[face_count, :] = embedding.astype('float32') (x, y, w, h) = dets[i].split(" ") writer.writerow([image_name, x, y, w, h]) face_count += 1 except: error = "Face crop " + str( i) + " of " + impath + " could not be embedded." except CouldNotReadFileError: error = "File could not be read." except NoFaceInImageError: error = "No Face in Image." errors.append(error) increment_embed_processed() if 'total_images' in body.keys(): h5_file.flush() h5_file.close()
def validate_post(self, body): if not all(k in body for k in("version", "status", "alerts")): raise HTTPBadRequest('missing version, status and/or alert attributes') if 'iris_plan' not in body["groupLabels"]: raise HTTPBadRequest('missing iris_plan in group labels')
def as_bad_request(self): return HTTPBadRequest(title="Validation failed deserialization failed", description=str(self))
def as_bad_request(self): return HTTPBadRequest(title="Representation deserialization failed", description=self._get_description())
def on_post(req, resp, team, roster): ''' See below for sample JSON requests. 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 } ''' 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 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`) 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)''' 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) 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 raise HTTPError('422 Unprocessable Entity', 'IntegrityError', err_msg) connection.commit() cursor.close() connection.close() resp.status = HTTP_201 resp.body = json_dumps({'id': schedule_id})
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(map(lambda x: x in roster_users, 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 get_schedules(filter_params, dbinfo=None, fields=None): """ Get schedule data for a request """ events = 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 'events' in fields: from_clause.append('LEFT JOIN `schedule_event` ON `schedule_event`.`schedule_id` = `schedule`.`id`') events = True 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 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() return data
def on_post(self, req, resp, auth_type): if auth_type == 'login': try: req_body = json.load(req.bounded_stream) if 'username' in req_body and req_body[ 'username'] and 'password' in req_body and len( req_body['password']) > 5: username = req_body['username'] redis_cli = super().redis_client if redis_cli is not None: user_info = redis_cli.hget('USERS', username) if user_info is not None: user_info = json.loads(user_info) api_key = user_info['api_key'] hash_password = hashlib.pbkdf2_hmac( 'sha256', req_body['password'].encode('utf-8'), api_key[8:32].encode('utf-8'), 100000, dklen=128) if user_info['password'] == hash_password.hex(): access_info = redis_cli.hget( 'USERS_APIKEY', api_key) access_info = json.loads(access_info) if access_info is not None and access_info[ 'is_active']: access_token = access_info['access_token'] if not redis_cli.exists(access_token): access_token = super().generate_token( 32) access_info[ 'access_token'] = access_token access_info = redis_cli.hset( 'USERS_APIKEY', api_key, json.dumps(access_info)) redis_cli.set(access_token, api_key, ex=28800) resp.status = HTTP_200 resp.body = json.dumps( dict(status='Success', user=dict( api_key=api_key, access_token=access_token, username=username))) else: raise HTTPNotAcceptable( description='User is not active') else: raise HTTPUnauthorized( description= 'Username and password doesnot match') else: raise HTTPNotFound( description='No user with username {}'.format( username)) else: raise HTTPServiceUnavailable( description='Data instances are not yet active') else: raise HTTPPreconditionFailed( description= 'Username and password are mandatory and must be valid for this request' ) except JSONDecodeError as err: print('Request body received', req.bounded_stream.read()) print('Error while processing request', err) raise HTTPUnprocessableEntity( description='Cannot parse the body from the request') except (HTTPPreconditionFailed, HTTPServiceUnavailable, HTTPNotFound, HTTPUnauthorized, HTTPNotAcceptable) as err: raise err except Exception as e: print('Exception in signing in user', e) raise HTTPInternalServerError( description='Something went wrong while creating user info' ) elif auth_type == 'logout': try: api_key = req.get_header('Authorization') redis_cli = super().redis_client if redis_cli is not None: access_info = redis_cli.hget('USERS_APIKEY', api_key) access_info = json.loads(access_info) access_token = access_info['access_token'] redis_cli.delete(access_token) resp.status = HTTP_200 resp.body = json.dumps( dict(status='Succcess', message='Successfully signed out')) else: raise HTTPServiceUnavailable( description='Data instances are not yet active') except Exception as err: print('Exception while signing out', err) raise HTTPInternalServerError( description='Something went wrong while signing out') else: raise HTTPBadRequest(description='The request is not valid')
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): """ 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_get(req, resp, team, roster, role): start = req.get_param_as_int('start', required=True) end = req.get_param_as_int('end', required=True) connection = db.connect() cursor = connection.cursor() try: cursor.execute('SELECT id FROM role WHERE name = %s', role) if cursor.rowcount == 0: raise HTTPBadRequest('Invalid role') role_id = cursor.fetchone()[0] cursor.execute( '''SELECT `team`.`id`, `roster`.`id` FROM `team` JOIN `roster` ON `roster`.`team_id` = `team`.`id` WHERE `roster`.`name` = %s and `team`.`name` = %s''', (roster, team)) if cursor.rowcount == 0: raise HTTPBadRequest('Invalid roster') team_id, roster_id = cursor.fetchone() cursor.execute('SELECT COUNT(*) FROM roster_user WHERE roster_id = %s', roster_id) if cursor.rowcount == 0: raise HTTPNotFound() roster_size = cursor.fetchone()[0] length = 604800 * roster_size data = { 'team_id': team_id, 'roster_id': roster_id, 'role_id': role_id, 'past': start - length, 'start': start, 'end': end, 'future': start + length } cursor.execute( '''SELECT `user`.`name` FROM `event` JOIN `user` ON `event`.`user_id` = `user`.`id` WHERE `team_id` = %(team_id)s AND %(start)s < `event`.`end` AND %(end)s > `event`.`start`''', data) busy_users = set(row[0] for row in cursor) cursor.execute( '''SELECT * FROM (SELECT `user`.`name` AS `user`, MAX(`event`.`start`) AS `before` FROM `roster_user` JOIN `user` ON `user`.`id` = `roster_user`.`user_id` AND roster_id = %(roster_id)s AND `roster_user`.`in_rotation` = 1 LEFT JOIN `event` ON `event`.`user_id` = `user`.`id` AND `team_id` = %(team_id)s AND `role_id` = %(role_id)s AND `start` BETWEEN %(past)s AND %(start)s GROUP BY `user`.`name`) past JOIN (SELECT `user`.`name` AS `user`, MIN(`event`.`start`) AS `after` FROM `roster_user` JOIN `user` ON `user`.`id` = `roster_user`.`user_id` AND roster_id = %(roster_id)s AND `roster_user`.`in_rotation` = 1 LEFT JOIN `event` ON `event`.`user_id` = `user`.`id` AND `team_id` = %(team_id)s AND `role_id` = %(role_id)s AND `start` BETWEEN %(start)s AND %(future)s GROUP BY `user`.`name`) future USING (`user`)''', data) candidate = None max_score = -1 # Find argmax(min(time between start and last event, time before start and next event)) # If no next/last event exists, set value to infinity # This should maximize gaps between shifts ret = {} for (user, before, after) in cursor: if user in busy_users: continue before = start - before if before is not None else float('inf') after = after - start if after is not None else float('inf') score = min(before, after) ret[user] = score if score != float('inf') else 'infinity' if score > max_score: candidate = user max_score = score finally: cursor.close() connection.close() resp.body = json_dumps({'user': candidate, 'data': ret})
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)
def on_post(self, req, resp): form_body = uri.parse_query_string(req.context['body'].decode('utf-8')) 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_api(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 return try: rendered_subject = subject_template.render(sample_context), rendered_body = body_template.render(sample_context) except Exception as e: resp.body = ujson.dumps({'error': str(e)}) resp.status = falcon.HTTP_400 return resp.body = ujson.dumps({ 'template_subject': rendered_subject, 'template_body': rendered_body })
def on_post(req, resp, schedule_id): """ Run the scheduler on demand from a given point in time. Deletes existing schedule events if applicable. Given the ``start`` param, this will find the first schedule start time after ``start``, then populate out to the schedule's auto_populate_threshold. It will also clear the calendar of any events associated with the chosen schedule from the start of the first event it created onward. For example, if `start` is Monday, May 1 and the chosen schedule starts on Wednesday, this will create events starting from Wednesday, May 3, and delete any events that start after May 3 that are associated with the schedule. **Example request:** .. sourcecode:: http POST api/v0/ HTTP/1.1 Content-Type: application/json :statuscode 200: Successful populate :statuscode 400: Validation checks failed """ # TODO: add images to docstring because it doesn't make sense data = load_json_body(req) start_time = data['start'] start_dt = datetime.fromtimestamp(start_time, utc) start_epoch = epoch_from_datetime(start_dt) # Get schedule info schedule = get_schedules({'id': schedule_id})[0] role_id = schedule['role_id'] team_id = schedule['team_id'] roster_id = schedule['roster_id'] first_event_start = min(schedule['events'], key=lambda x: x['start'])['start'] period = get_period_len(schedule) handoff = start_epoch + timedelta(seconds=first_event_start) handoff = timezone(schedule['timezone']).localize(handoff) # Start scheduling from the next occurrence of the hand-off time. if start_dt > handoff: start_epoch += timedelta(weeks=period) handoff += timedelta(weeks=period) if handoff < utc.localize(datetime.utcnow()): raise HTTPBadRequest('Invalid populate request', 'cannot populate starting in the past') check_team_auth(schedule['team'], req) connection = db.connect() cursor = connection.cursor(db.DictCursor) future_events, last_epoch = calculate_future_events( schedule, cursor, start_epoch) set_last_epoch(schedule_id, last_epoch, cursor) # Delete existing events from the start of the first event future_events = [ filter(lambda x: x['start'] >= start_time, evs) for evs in future_events ] future_events = filter(lambda x: x != [], future_events) if future_events: first_event_start = min(future_events[0], key=lambda x: x['start'])['start'] cursor.execute( 'DELETE FROM event WHERE schedule_id = %s AND start >= %s', (schedule_id, first_event_start)) # Create events in the db, associating a user to them for epoch in future_events: user_id = find_least_active_available_user_id(team_id, role_id, roster_id, epoch, cursor) if not user_id: continue create_events(team_id, schedule['id'], user_id, epoch, role_id, cursor) connection.commit() cursor.close() connection.close()
def validate_post(self, body): if not all(k in body for k in("ruleName", "state")): logger.warning('missing ruleName and/or state attributes') raise HTTPBadRequest('missing ruleName and/of state attributes')
def on_post(self, req, resp): ''' This endpoint is compatible with the webhook post from Grafana. Simply configure Grafana with a new notification channel with type 'webhook' and a plan parameter pointing to your iris plan. Name: 'iris-team1' Url: http://iris:16649/v0/webhooks/grafana?application=test-app&key=sdffdssdf&plan=team1 ''' alert_params = ujson.loads(req.context['body']) self.validate_post(alert_params) with db.guarded_session() as session: plan = req.get_param('plan', True) plan_id = session.execute( 'SELECT `plan_id` FROM `plan_active` WHERE `name` = :plan', { 'plan': plan }).scalar() if not plan_id: logger.warn('No active plan "%s" found', plan) raise HTTPInvalidParam('plan does not exist or is not active') 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, } 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
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(req, resp): """ Endpoint for creating linked events. Responds with event ids for created events. Linked events can be swapped in a group, and users are reminded only on the first event of a linked series. Linked events have a link_id attribute containing a uuid. All events with an equivalent link_id are considered "linked together" in a single set. Editing any single event in the set will break the link for that event, clearing the link_id field. Otherwise, linked events behave the same as any non-linked event. **Example request:** .. sourcecode:: http POST /api/v0/events/link HTTP/1.1 Content-Type: application/json [ { "start": 1493667700, "end": 149368700, "user": "******", "team": "team-foo", "role": "primary", }, { "start": 1493677700, "end": 149387700, "user": "******", "team": "team-foo", "role": "primary", } ] **Example response:** .. sourcecode:: http HTTP/1.1 201 Created Content-Type: application/json [1, 2] :statuscode 201: Event created :statuscode 400: Event validation checks failed :statuscode 422: Event creation failed: nonexistent role/event/team """ events = load_json_body(req) if not isinstance(events, list): raise HTTPBadRequest('Invalid argument', 'events argument needs to be a list') if not events: raise HTTPBadRequest('Invalid argument', 'events list cannot be empty') now = time.time() team = events[0].get('team') if not team: raise HTTPBadRequest('Invalid argument', 'event missing team attribute') check_calendar_auth(team, req) event_values = [] link_id = gen_link_id() connection = db.connect() cursor = connection.cursor() columns = ('`start`', '`end`', '`user_id`', '`team_id`', '`role_id`', '`link_id`') try: cursor.execute('SELECT `id` FROM `team` WHERE `name`=%s', team) team_id = cursor.fetchone() if not team_id: raise HTTPBadRequest('Invalid event', 'Invalid team name: %s' % team) values = [ '%s', '%s', '(SELECT `id` FROM `user` WHERE `name`=%s)', '%s', '(SELECT `id` FROM `role` WHERE `name`=%s)', '%s' ] for ev in events: if ev['end'] < now: raise HTTPBadRequest('Invalid event', 'Creating events in the past not allowed') if ev['start'] >= ev['end']: raise HTTPBadRequest('Invalid event', 'Event must start before it ends') ev_team = ev.get('team') if not ev_team: raise HTTPBadRequest('Invalid event', 'Missing team for event') if team != ev_team: raise HTTPBadRequest('Invalid event', 'Events can only be submitted to one team') if not user_in_team_by_name(cursor, ev['user'], team): raise HTTPBadRequest('Invalid event', 'User %s must be part of the team %s' % (ev['user'], team)) event_values.append((ev['start'], ev['end'], ev['user'], team_id, ev['role'], link_id)) insert_query = 'INSERT INTO `event` (%s) VALUES (%s)' % (','.join(columns), ','.join(values)) cursor.executemany(insert_query, event_values) connection.commit() cursor.execute('SELECT `id` FROM `event` WHERE `link_id`=%s', link_id) ev_ids = [row[0] for row in cursor] except db.IntegrityError as e: err_msg = str(e.args[1]) if err_msg == 'Column \'role_id\' cannot be null': err_msg = 'role not found' elif err_msg == 'Column \'user_id\' cannot be null': err_msg = 'user 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) finally: cursor.close() connection.close() resp.status = HTTP_201 resp.body = json_dumps(ev_ids)
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_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