コード例 #1
0
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()
コード例 #2
0
 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.'
                 )
コード例 #3
0
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})
コード例 #4
0
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
コード例 #5
0
    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.'))
コード例 #6
0
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
コード例 #7
0
ファイル: user_notification.py プロジェクト: houqp/oncall
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()
コード例 #8
0
ファイル: decorators.py プロジェクト: ukor/falcon-graphene
 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)
コード例 #9
0
ファイル: utils.py プロジェクト: cleveritcz/oncall
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))
コード例 #10
0
    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)
コード例 #11
0
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()
コード例 #12
0
    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')
コード例 #13
0
ファイル: errors.py プロジェクト: sys-git/graceful
 def as_bad_request(self):
     return HTTPBadRequest(title="Validation failed deserialization failed",
                           description=str(self))
コード例 #14
0
ファイル: errors.py プロジェクト: sys-git/graceful
 def as_bad_request(self):
     return HTTPBadRequest(title="Representation deserialization failed",
                           description=self._get_description())
コード例 #15
0
ファイル: schedules.py プロジェクト: manufacturedba/oncall
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})
コード例 #16
0
ファイル: roster.py プロジェクト: tymiles003/oncall-2
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()
コード例 #17
0
ファイル: schedules.py プロジェクト: manufacturedba/oncall
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
コード例 #18
0
ファイル: Auth.py プロジェクト: sriinath/sheets-v4
 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')
コード例 #19
0
 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())
コード例 #20
0
ファイル: events.py プロジェクト: arpanchaudhury/oncall
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)
コード例 #21
0
ファイル: roster_suggest.py プロジェクト: zhuyezoie/oncall
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})
コード例 #22
0
ファイル: events.py プロジェクト: arpanchaudhury/oncall
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)
コード例 #23
0
    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
        })
コード例 #24
0
ファイル: populate.py プロジェクト: twoflowers/oncall
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()
コード例 #25
0
 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')
コード例 #26
0
ファイル: grafana.py プロジェクト: wayfair-contribs/iris
    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
コード例 #27
0
ファイル: roster_users.py プロジェクト: zhuyezoie/oncall
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])
コード例 #28
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)
コード例 #29
0
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
コード例 #30
0
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