1
0
mirror of https://github.com/linkedin/oncall.git synced 2025-11-28 23:20:23 +02:00

Document everything

This commit is contained in:
Daniel Wang
2017-05-19 18:07:35 -07:00
committed by Qingping Hou
parent 9777768235
commit 6558d1f388
28 changed files with 1406 additions and 56 deletions

View File

@@ -137,7 +137,7 @@ CREATE TABLE IF NOT EXISTS `schedule` (
ON UPDATE CASCADE);
-- -----------------------------------------------------
-- Table `schedule_events`
-- Table `schedule_event`
-- -----------------------------------------------------
CREATE TABLE IF NOT EXISTS `schedule_event` (
`id` BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,

View File

@@ -67,7 +67,7 @@ def on_get(req, resp):
"""
http:get:: /api/v0/events
Search for events.
Search for events. Allows filtering based on a number of parameters, detailed below.
**Example request**:
@@ -120,6 +120,21 @@ def on_get(req, resp):
: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: role name
: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: team name
: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 user: user name
: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
@@ -157,13 +172,22 @@ def on_get(req, resp):
@login_required
def on_post(req, resp):
"""
Endpoint for creating event. Responds with event id for created event
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 /v0/events HTTP/1.1
POST api/v0/events HTTP/1.1
Content-Type: application/json
{

View File

@@ -16,7 +16,7 @@ from pytz import timezone, utc
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
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

View File

@@ -14,7 +14,25 @@ from ...constants import ROSTER_USER_ADDED
def on_get(req, resp, team, roster):
"""
Get all users for a team roster
http:get:: /api/v0/teams/team-foo/rosters/roster-foo/users
Get all users for a team's roster
**Example request**:
.. sourcecode:: http
GET /api/v0/teams/team-foo/rosters/roster-foo/users HTTP/1.1
Host: example.com
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Content-Type: application/json
["jdoe", "asmith"]
"""
team, roster = unquote(team), unquote(roster)
connection = db.connect()
@@ -36,7 +54,47 @@ def on_get(req, resp, team, roster):
@login_required
def on_post(req, resp, team, roster):
"""
Add user to a roster for a team
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": "jdoe@example.com",
"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)

View File

@@ -60,7 +60,63 @@ def get_roster_by_team_id(cursor, team_id, params=None):
def on_get(req, resp, team):
"""
Get roster info(including schedules) for a team
Get roster info for a team. Returns a JSON object with roster names
as keys, and info as values. This info includes the roster id, any
schedules associated with the rosters, and roster users (along
with their status as in/out of rotation).
**Example request**:
.. sourcecode:: http
GET /api/v0/teams/team-foo/rosters HTTP/1.1
Host: example.com
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Content-Type: application/json
{
"roster-foo": {
"id": 2923,
"schedules": [
{
"advanced_mode": 0,
"auto_populate_threshold": 30,
"events": [
{
"duration": 604800,
"start": 266400
}
],
"id": 1788,
"role": "primary",
"role_id": 1,
"roster": "roster-foo",
"roster_id": 2923,
"team": "team-foo",
"team_id": 2122,
"timezone": "US/Pacific"
}
],
"users": [
{
"in_rotation": true,
"name": "jdoe"
},
{
"in_rotation": true,
"name": "asmith"
}
]
}
}
:statuscode 422: Invalid team
"""
team = unquote(team)
connection = db.connect()
@@ -84,6 +140,28 @@ def on_get(req, resp, team):
def on_post(req, resp, team):
"""
Create a roster for a team
**Example request:**
.. sourcecode:: http
POST /v0/teams/team-foo/rosters HTTP/1.1
Content-Type: application/json
{
"name": "roster-foo",
}
**Example response:**
.. sourcecode:: http
HTTP/1.1 201 Created
Content-Type: application/json
:statuscode 201: Succesful roster creation
:statuscode 422: Invalid character in roster name/Duplicate roster name
"""
team = unquote(team)
data = load_json_body(req)

View File

@@ -37,13 +37,77 @@ def verify_auth(req, schedule_id, connection, cursor):
def on_get(req, resp, schedule_id):
"""
Get schedule information. Detailed information on schedule parameters is provided in the
POST method for /api/v0/team/{team_name}/rosters/{roster_name}/schedules.
**Example request**:
.. sourcecode:: http
GET /api/v0/schedules/1234 HTTP/1.1
Host: example.com
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Content-Type: application/json
{
"advanced_mode": 1,
"auto_populate_threshold": 30,
"events": [
{
"duration": 259200,
"start": 0
}
],
"id": 1234,
"role": "primary",
"role_id": 1,
"roster": "roster-foo",
"roster_id": 2922,
"team": "asdf",
"team_id": 2121,
"timezone": "US/Pacific"
}
"""
resp.body = json_dumps(get_schedules({'schedule_id': schedule_id}, fields=req.get_param_as_list('fields'))[0])
@login_required
def on_put(req, resp, schedule_id):
"""
Update a schedule
Update a schedule. Allows editing of role, team, roster, auto_populate_threshold,
events, and advanced_mode. Only allowed for team admins. Note that simple mode
schedules must conform to simple schedule restrictions (described in documentation
for the /api/v0/team/{team_name}/rosters/{roster_name}/schedules GET endpoint).
This is checked on both "events" and "advanced_mode" edits.
**Example request:**
.. sourcecode:: http
PUT /api/v0/schedules/1234 HTTP/1.1
Content-Type: application/json
{
"role": "primary",
"team": "team-bar",
"roster": "roster-bar",
"auto_populate_threshold": 28,
"events":
[
{
"start": 0,
"duration": 100
}
]
"advanced_mode": 1
}
"""
data = load_json_body(req)
@@ -90,7 +154,16 @@ def on_put(req, resp, schedule_id):
@login_required
def on_delete(req, resp, schedule_id):
"""
Delete a schedule
Delete a schedule by id. Only allowed for team admins.
**Example request:**
.. sourcecode:: http
DELETE /api/v0/schedules/1234 HTTP/1.1
:statuscode 200: Successful delete
:statuscode 404: Schedule not found
"""
connection = db.connect()
cursor = connection.cursor()

View File

@@ -71,7 +71,16 @@ def validate_simple_schedule(events):
def get_schedules(filter_params, dbinfo=None, fields=None):
"""
Get schedule data for a request
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
from_clause = ['`schedule`']
@@ -133,9 +142,13 @@ def get_schedules(filter_params, dbinfo=None, fields=None):
def insert_schedule_events(schedule_id, events, cursor):
"""
Helper to insert schedule events for a schedule
"""
insert_events = '''INSERT INTO `schedule_event` (`schedule_id`, `start`, `duration`)
VALUES (%(schedule)s, %(start)s, %(duration)s)'''
# Merge consecutive events for db storage
# Merge consecutive events for db storage. This creates an equivalent, simpler
# form of the schedule for the scheduler.
raw_events = sorted(events, key=lambda e: e['start'])
new_events = []
for e in raw_events:
@@ -149,6 +162,71 @@ def insert_schedule_events(schedule_id, events, cursor):
def on_get(req, resp, team, roster):
"""
Get schedules for a given roster. Information on schedule attributes is detailed
in the schedules POST endpoint documentation. Schedules can be filtered with
the following parameters passed in the query string:
:query id: id of the schedule
:query id__eq: id of the schedule
:query id__gt: id greater than
:query id__ge: id greater than or equal
:query id__lt: id less than
:query id__le: id less than or equal
:query name: schedule name
:query name__eq: schedule name
:query name__contains: schedule name contains param
:query name__startswith: schedule name starts with param
:query name__endswith: schedule name ends with param
:query role: schedule role name
:query role__eq: schedule role name
:query role__contains: schedule role name contains param
:query role__startswith: schedule role name starts with param
:query role__endswith: schedule role name ends with param
:query team: schedule team name
:query team__eq: schedule team name
:query team__contains: schedule team name contains param
:query team__startswith: schedule team name starts with param
:query team__endswith: schedule team name ends with param
:query team_id: id of the schedule's team
:query roster_id: id of the schedule's roster
**Example request**:
.. sourcecode:: http
GET /api/v0/teams/team-foo/rosters/roster-foo/schedules HTTP/1.1
Host: example.com
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Content-Type: application/json
[
{
"advanced_mode": 1,
"auto_populate_threshold": 30,
"events": [
{
"duration": 259200,
"start": 0
}
],
"id": 2065,
"role": "primary",
"role_id": 1,
"roster": "roster-foo",
"roster_id": 2922,
"team": "team-foo",
"team_id": 2121,
"timezone": "US/Pacific"
}
]
"""
team = unquote(team)
roster = unquote(roster)
fields = req.get_param_as_list('fields')
@@ -169,8 +247,38 @@ required_params = frozenset(['events', 'role', 'advanced_mode'])
@login_required
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
@@ -199,6 +307,40 @@ def on_post(req, resp, team, roster):
],
'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)

View File

@@ -6,6 +6,70 @@ from ujson import dumps
def on_get(req, resp):
'''
Endpoint for searching for teams, services, users, and team users by keyword. Used for
typeaheads in the frontend. Team/service search is done using substring matching, while
user and team_user search is done with prefix matching. If no fields are provided, the
endpoint defaults to ['teams', 'services']. A keyword parameter must be passed in the
query string.
**Example request**
.. sourcecode:: http
GET /api/v0/search?keyword=key HTTP/1.1
Host: example.com
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Content-Type: application/json
{
"services": [
{
"service-key": "team-foo"
},
{
"service-key2": "team-bar"
}
]
"teams": [
"team-key",
"team-key2"
]
}
An example for user search:
**Example request**
.. sourcecode:: http
GET /api/v0/search?keyword=key&fields=users HTTP/1.1
Host: example.com
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Content-Type: application/json
{
"users":
[
{
"full_name": "John Doe",
"name": "jdoe"
}
]
}
'''
keyword = req.get_param('keyword', required=True)
fields = req.get_param_as_list('fields')
if not fields:

View File

@@ -12,6 +12,27 @@ from ...auth import debug_only
def on_get(req, resp, service):
"""
Get service id by name
**Example request**
.. sourcecode:: http
GET /api/v0/services/service-foo HTTP/1.1
Host: example.com
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Content-Type: application/json
{
"id": 1234,
"name": "service-foo"
}
"""
connection = db.connect()
cursor = connection.cursor(db.DictCursor)
@@ -28,7 +49,7 @@ def on_get(req, resp, service):
@debug_only
def on_put(req, resp, service):
"""
Change name for a service
Change name for a service. Currently unused/debug only.
"""
data = load_json_body(req)
connection = db.connect()
@@ -43,7 +64,7 @@ def on_put(req, resp, service):
@debug_only
def on_delete(req, resp, service):
"""
Delete a service
Delete a service. Currently unused/debug only.
"""
connection = db.connect()
cursor = connection.cursor()

View File

@@ -6,6 +6,40 @@ from ... import db
def on_get(req, resp, service, role):
'''
Get the current user on-call for a given service/role. Returns event start/end, contact info,
and user name.
**Example request**
.. sourcecode:: http
GET /api/v0/services/service-foo/oncall/primary HTTP/1.1
Host: example.com
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Content-Type: application/json
[
{
"contacts": {
"call": "+1 111-111-1111",
"email": "jdoe@example.com",
"im": "jdoe",
"sms": "+1 111-111-1111"
},
"end": 1495695600,
"start": 1495263600,
"user": "John Doe"
}
]
'''
get_oncall_query = '''SELECT `user`.`full_name` AS `user`, `event`.`start`, `event`.`end`,
`contact_mode`.`name` AS `mode`, `user_contact`.`destination`
FROM `service` JOIN `team_service` ON `service`.`id` = `team_service`.`service_id`

View File

@@ -8,6 +8,25 @@ from ... import db
def on_get(req, resp, service):
"""
Get list of team mapped to a service
**Example request**
.. sourcecode:: http
GET /api/v0/services/service-foo/teams HTTP/1.1
Host: example.com
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Content-Type: application/json
[
"team-foo"
]
"""
connection = db.connect()
cursor = connection.cursor()

View File

@@ -26,7 +26,38 @@ constraints = {
def on_get(req, resp):
"""
Search for services
Find services, filtered by params
:query id: id of the service
:query id__eq: id of the service
:query id__gt: id greater than
:query id__ge: id greater than or equal
:query id__lt: id less than
:query id__le: id less than or equal
:query name: service name
:query name__eq: service name
:query name__contains: service name contains param
:query name__startswith: service name starts with param
:query name__endswith: service name ends with param
**Example request**
.. sourcecode:: http
GET /api/v0/services?name__startswith=service HTTP/1.1
Host: example.com
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Content-Type: application/json
[
"service-foo"
]
"""
query = 'SELECT `name` FROM `service`'

View File

@@ -54,6 +54,91 @@ populate_map = {
def on_get(req, resp, team):
'''
Get team info by name. By default, only finds active teams. Allows selection of
fields, including: users, admins, services, and rosters. If no ``fields`` is
specified in the query string, it defaults to all fields.
**Example request**
.. sourcecode:: http
GET /api/v0/teams/team-foo HTTP/1.1
Host: example.com
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Content-Type: application/json
{
"admins": [
{
"name": "jdoe"
}
],
"email": "foo@example.com",
"id": 5501,
"iris_plan": null,
"name": "team-foo",
"rosters": {
"roster-foo": {
"id": 4186,
"schedules": [
{
"advanced_mode": 0,
"auto_populate_threshold": 21,
"events": [
{
"duration": 604800,
"start": 7200
}
],
"id": 2222,
"role": "primary",
"role_id": 1,
"roster": "roster-foo",
"roster_id": 4186,
"team": "team-foo",
"team_id": 5501,
"timezone": "US/Pacific"
}
],
"users": [
{
"in_rotation": true,
"name": "jdoe"
}
]
}
},
"scheduling_timezone": "US/Pacific",
"services": [
"service-foo"
],
"slack_channel": "#foo",
"users": {
"jdoe": {
"active": 1,
"contacts": {
"call": "+1 111-111-1111",
"email": "jdoe@example.com",
"im": "jdoe",
"sms": "+1 111-111-1111"
},
"full_name": "John Doe",
"id": 1234,
"name": "jdoe",
"photo_url": "image.example.com",
"time_zone": "US/Pacific"
}
}
}
'''
team = unquote(team)
fields = req.get_param_as_list('fields')
active = req.get_param('active', default=True)
@@ -83,6 +168,27 @@ def on_get(req, resp, team):
@login_required
def on_put(req, resp, team):
'''
Edit a team's information. Allows edit of: name, slack_channel, email, scheduling_timezone, iris_plan.
**Example request:**
.. sourcecode:: http
PUT /api/v0/teams/team-foo HTTP/1.1
Content-Type: application/json
{
"name": "team-bar",
"slack_channel": "roster-bar",
"email": 28,
"scheduling_timezone": "US/Central"
}
:statuscode 200: Successful edit
:statuscode 400: Invalid team name/iris escalation plan
:statuscode 422: Duplicate team name
'''
team = unquote(team)
check_team_auth(team, req)
data = load_json_body(req)
@@ -122,6 +228,20 @@ def on_put(req, resp, team):
@login_required
def on_delete(req, resp, team):
'''
Soft delete for teams. Does not remove data from the database, but sets the team's active
param to false. Note that this means deleted teams' names remain in the namespace, so new
teams cannot be created with the same name a sa deleted team.
**Example request:**
.. sourcecode:: http
DELETE /api/v0/teams/team-foo HTTP/1.1
:statuscode 200: Successful delete
:statuscode 404: Team not found
'''
team = unquote(team)
check_team_auth(team, req)
connection = db.connect()

View File

@@ -12,7 +12,16 @@ from ...constants import ADMIN_DELETED
@login_required
def on_delete(req, resp, team, user):
"""
Delete team admin user
Delete team admin user. Removes admin from the team if he/she is not a member of any roster.
**Example request:**
.. sourcecode:: http
DELETE /api/v0/teams/team-foo/admins/jdoe HTTP/1.1
:statuscode 200: Successful delete
:statuscode 404: Team admin not found
"""
check_team_auth(team, req)
connection = db.connect()

View File

@@ -13,7 +13,27 @@ from ...constants import ADMIN_CREATED
def on_get(req, resp, team):
"""
Get list of admin users for a team
Get list of admin usernames for a team
**Example request**
.. sourcecode:: http
GET /api/v0/teams/team-foo/admins HTTP/1.1
Host: example.com
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Content-Type: application/json
[
"jdoe",
"asmith"
]
"""
team = unquote(team)
connection = db.connect()
@@ -32,7 +52,43 @@ def on_get(req, resp, team):
@login_required
def on_post(req, resp, team):
"""
Add user as team admin
Add user as a team admin. Responds with that user's info (similar to user GET).
Subscribes this user to default notifications for the team, and adds the user
to the team (if needed).
**Example request**
.. sourcecode:: http
POST /api/v0/teams/team-foo/admins HTTP/1.1
Host: example.com
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Content-Type: application/json
{
"active": 1,
"contacts": {
"call": "+1 111-111-1111",
"email": "jdoe@example.com",
"im": "jdoe",
"sms": "+1 111-111-1111"
},
"full_name": "John Doe",
"id": 9535,
"name": "jdoe",
"photo_url": "image.example.com",
"time_zone": "US/Pacific"
}
:statuscode 201: Successful admin added
:statuscode 400: Missing name attribute in request
:statuscode 422: Invalid team/user, or user is already a team admin
"""
team = unquote(team)
check_team_auth(team, req)
@@ -46,7 +102,7 @@ def on_post(req, resp, team):
cursor = connection.cursor()
cursor.execute('''(SELECT `id` FROM `team` WHERE `name`=%s)
UNION
UNION ALL
(SELECT `id` FROM `user` WHERE `name`=%s)''', (team, user_name))
results = [r[0] for r in cursor]
if len(results) < 2:

View File

@@ -9,7 +9,23 @@ from requests import ConnectionError
def on_post(req, resp, team):
'''
Escalate to a team using the team's configured Iris plan. Configured in the
'iris_plan_integration' section of the configuration file.
'iris_plan_integration' section of the configuration file. If iris plan integration
is not activated, this endpoint will be disabled.
**Example request:**
.. sourcecode:: http
POST /v0/events HTTP/1.1
Content-Type: application/json
{
"description": "Something bad happened!",
}
:statuscode 200: Incident created
:statuscode 400: Escalation failed, missing description/No escalation plan specified
for team/Iris client error.
'''
data = load_json_body(req)

View File

@@ -12,7 +12,16 @@ from ... import db
@login_required
def on_delete(req, resp, team, service):
"""
Delete service team mapping
Delete service team mapping. Only allowed for team admins.
**Example request:**
.. sourcecode:: http
DELETE /api/v0/teams/team-foo/services/service-foo HTTP/1.1
:statuscode 200: Successful delete
:statuscode 404: Team-service mapping not found
"""
team = unquote(team)
check_team_auth(team, req)

View File

@@ -13,6 +13,25 @@ from ... import db
def on_get(req, resp, team):
"""
Get list of services mapped to a team
**Example request**:
.. sourcecode:: http
GET /api/v0/teams/team-foo/services HTTP/1.1
Host: example.com
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Content-Type: application/json
[
"service-foo",
"service-bar"
]
"""
team = unquote(team)
connection = db.connect()
@@ -31,7 +50,31 @@ def on_get(req, resp, team):
@login_required
def on_post(req, resp, team):
"""
Create team to service mapping
Create team to service mapping. Takes an object defining "name", then maps
that service to the team specified in the URL. Note that this endpoint does
not create a service; it expects this service to already exist.
**Example request:**
.. sourcecode:: http
POST api/v0/teams/team-foo/services HTTP/1.1
Content-Type: application/json
{
"name": "service-foo",
}
**Example response:**
.. sourcecode:: http
HTTP/1.1 201 Created
Content-Type: application/json
:statuscode 201: Successful create
:statuscode 422: Mapping creation failed; Possible errors: Invalid service/team name,
service already mapped to the team, service mapped to another team
"""
team = unquote(team)
check_team_auth(team, req)
@@ -50,7 +93,7 @@ def on_post(req, resp, team):
if claimed_team:
raise HTTPError('422 Unprocessable Entity',
'IntegrityError',
'service "%s" alread claimed by team "%s"' % (service, claimed_team[0]))
'service "%s" already claimed by team "%s"' % (service, claimed_team[0]))
cursor.execute('''INSERT INTO `team_service` (`team_id`, `service_id`)
VALUES (

View File

@@ -8,6 +8,105 @@ from falcon import HTTPNotFound
def on_get(req, resp, team):
'''
Endpoint to get a summary of the team's oncall information. Returns an object
containing the fields ``current`` and ``next``, which then contain information
on the current and next on-call shifts for this team. ``current`` and ``next``
are objects keyed by role (if an event of that role exists), with values of
lists of event/user information. This list will have multiple elements if
multiple events with the same role are currently occurring, or if multiple
events with the same role are starting next in the future at the same time.
If no event with a given role exists, that role is excluded from the ``current``
or ``next`` object. If no events exist, the ``current`` and ``next`` objects
will be empty objects.
**Example request:**
.. sourcecode:: http
GET api/v0/teams/team-foo/summary HTTP/1.1
Content-Type: application/json
**Example response:**
.. sourcecode:: http
HTTP/1.1 200 OK
Content-Type: application/json
{
"current": {
"manager": [
{
"end": 1495760400,
"full_name": "John Doe",
"photo_url": "example.image.com",
"role": "manager",
"start": 1495436400,
"user_contacts": {
"call": "+1 111-111-1111",
"email": "jdoe@example.com",
"im": "jdoe",
"sms": "+1 111-111-1111"
},
"user_id": 1234
}
],
"primary": [
{
"end": 1495760400,
"full_name": "Adam Smith",
"photo_url": "example.image.com",
"role": "primary",
"start": 1495350000,
"user_contacts": {
"call": "+1 222-222-2222",
"email": "asmith@example.com",
"im": "asmith",
"sms": "+1 222-222-2222"
},
"user_id": 1235
}
]
},
"next": {
"manager": [
{
"end": 1496127600,
"full_name": "John Doe",
"photo_url": "example.image.com",
"role": "manager",
"start": 1495436400,
"user_contacts": {
"call": "+1 111-111-1111",
"email": "jdoe@example.com",
"im": "jdoe",
"sms": "+1 111-111-1111"
},
"user_id": 1234
}
],
"primary": [
{
"end": 1495760400,
"full_name": "Adam Smith",
"photo_url": "example.image.com",
"role": "primary",
"start": 1495350000,
"user_contacts": {
"call": "+1 222-222-2222",
"email": "asmith@example.com",
"im": "asmith",
"sms": "+1 222-222-2222"
},
"user_id": 1235
}
]
}
}
'''
connection = db.connect()
cursor = connection.cursor(db.DictCursor)

View File

@@ -13,6 +13,15 @@ from ... import db
def on_delete(req, resp, team, user):
"""
Delete user from a team
**Example request:**
.. sourcecode:: http
DELETE /api/v0/teams/team-foo/users/jdoe HTTP/1.1
:statuscode 200: Successful delete
:statuscode 404: User not found in team
"""
team = unquote(team)
check_team_auth(team, req)

View File

@@ -13,7 +13,28 @@ constraints = {'active': '`team`.`active` = %s'}
def on_get(req, resp, team):
"""
Get list of users for a team
Get list of usernames for all team members. A user is a member of a team when
he/she is a team admin or a member of one of the team's rosters. Accepts an
``active`` parameter in the query string that filters inactive (deleted) teams.
**Example request:**
.. sourcecode:: http
GET /api/v0/teams/team-foo/users HTTP/1.1
Content-Type: application/json
**Example response:**
.. sourcecode:: http
HTTP/1.1 200 OK
Content-Type: application/json
[
"jdoe",
"asmith"
]
"""
query = '''SELECT `user`.`name` FROM `user`
JOIN `team_user` ON `team_user`.`user_id`=`user`.`id`
@@ -37,7 +58,7 @@ def on_get(req, resp, team):
@login_required
def on_post(req, resp, team):
"""
Add user to a team
Add user to a team. Deprecated; used only for testing purposes.
"""
check_team_auth(team, req)
data = load_json_body(req)

View File

@@ -23,6 +23,42 @@ constraints = {
def on_get(req, resp):
'''
http:get:: /api/v0/teams
Search for team names. Allows filtering based on a number of parameters, detailed below.
Returns list of matching team names. If "active" parameter is unspecified, defaults to
True (only displaying undeleted teams)
:query name: team name
:query name__eq: team name
:query name__contains: team name contains param
:query name__startswith: team name starts with param
:query name__endswith: team name ends with param
:query id: team id
:query id__eq: team id
:query active: team active/deleted (1 and 0, respectively)
**Example request**:
.. sourcecode:: http
GET /api/v0/teams?name__startswith=team- HTTP/1.1
Host: example.com
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Content-Type: application/json
[
"team-foo",
"team-bar"
]
'''
query = 'SELECT `name`, `email`, `slack_channel`, `scheduling_timezone`, `iris_plan` FROM `team`'
if 'active' not in req.params:
@@ -50,6 +86,48 @@ def on_get(req, resp):
@login_required
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": "team-foo@example.com",
"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')

View File

@@ -7,6 +7,46 @@ from ujson import dumps as json_dumps
def on_get(req, resp, user_name):
'''
Endpoint for retrieving a user's upcoming shifts. Groups linked events into a single
entity, with the number of events indicated in the ``num_events`` attribute. Non-linked
events have ``num_events = 0``. Returns a list of event information for each of that
user's upcoming shifts. Results can be filtered with the query string params below:
:query limit: The number of shifts to retrieve. Default is unlimited
:query role: Filters results to return only shifts with the provided roles.
**Example request**:
.. sourcecode:: http
GET /api/v0/users/jdoe/upcoming HTTP/1.1
Host: example.com
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Content-Type: application/json
[
{
"end": 1496264400,
"full_name": "John Doe",
"id": 169877,
"link_id": "7b3b96279bb24de8ac3fb7dbf06e5d1e",
"num_events": 7,
"role": "primary",
"schedule_id": 1788,
"start": 1496221200,
"team": "team-foo",
"user": "jdoe"
}
]
'''
connection = db.connect()
cursor = connection.cursor(db.DictCursor)
role = req.get_param('role', None)

View File

@@ -11,7 +11,49 @@ from .users import columns, get_user_data
def on_get(req, resp, user_name):
"""
Get user info by name
Get user info by name. Retrieved fields can be filtered with the ``fields``
query parameter. Valid fields:
- id - user id
- name - username
- contacts - user contact information
- full_name - user's full name
- time_zone - user's preferred display timezone
- photo_url - URL of user's thumbnail photo
- active - bool indicating whether the user is active in Oncall. Users can
be marked inactive after leaving the company to preserve past event information.
If no ``fields`` is provided, the endpoint defaults to returning all fields.
**Example request**:
.. sourcecode:: http
GET /api/v0/users/jdoe HTTP/1.1
Host: example.com
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Content-Type: application/json
{
"active": 1,
"contacts": {
"call": "+1 111-111-1111",
"email": "jdoe@example.com",
"im": "jdoe",
"sms": "+1 111-111-1111"
},
"full_name": "John Doe",
"id": 1234,
"name": "jdoe",
"photo_url": "image.example.com",
"time_zone": "US/Pacific"
}
"""
# Format request to filter query on user name
req.params['name'] = user_name
@@ -25,6 +67,15 @@ def on_get(req, resp, user_name):
def on_delete(req, resp, user_name):
"""
Delete user by name
**Example request:**
.. sourcecode:: http
DELETE /api/v0/users/jdoe HTTP/1.1
:statuscode 200: Successful delete
:statuscode 404: User not found
"""
check_user_auth(user_name, req)
connection = db.connect()
@@ -38,7 +89,38 @@ def on_delete(req, resp, user_name):
@login_required
def on_put(req, resp, user_name):
"""
Update user info
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": "jdoe@example2.com"
}
"name": "johndoe",
"full_name": "Johnathan Doe",
}
:statuscode 204: Successful edit
:statuscode 404: User not found
"""
contacts_query = '''INSERT INTO user_contact (`user_id`, `mode_id`, `destination`) VALUES
((SELECT `id` FROM `user` WHERE `name` = %(user)s),

View File

@@ -15,6 +15,18 @@ columns = {'team': '`team_id` = (SELECT `id` FROM `team` WHERE `name` = %s)',
@login_required
def on_delete(req, resp, notification_id):
'''
Delete user notification settings by id.
**Example request:**
.. sourcecode:: http
DELETE /api/v0/notifications/1234 HTTP/1.1
:statuscode 200: Successful delete
:statuscode 404: Notification setting not found
'''
connection = db.connect()
cursor = connection.cursor()
try:
@@ -38,6 +50,34 @@ def on_delete(req, resp, notification_id):
@login_required
def on_put(req, resp, notification_id):
'''
Edit user notification settings. Allows editing of the following attributes:
- roles: list of role names
- team: team name
- mode: contact mode name
- type: string defining what event to notify on. Types are detailed in notification
POST documentation
- time_before: in units of seconds (if reminder setting)
- only_if_involved: boolean (if notification setting)
**Example request**
.. sourcecode:: http
PUT /api/v0/events/1234 HTTP/1.1
Content-Type: application/json
{
"team": "team-bar",
"mode": "call",
"user": "asmith",
"roles": ["secondary"]
}
:statuscode 200: Successful edit
:statuscode 400: Validation checks failed.
'''
data = load_json_body(req)
params = data.keys()
roles = data.pop('roles')

View File

@@ -13,6 +13,55 @@ all_params = required_params | other_params
def on_get(req, resp, user_name):
'''
Get all notification settings for a user by name.
**Example request**:
.. sourcecode:: http
GET /api/v0/users/jdoe/notifications HTTP/1.1
Host: example.com
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Content-Type: application/json
[
{
"id": 21830,
"mode": "email",
"only_if_involved": null,
"roles": [
"primary",
"secondary",
"shadow",
"manager"
],
"team": "team-foo",
"time_before": 86400,
"type": "oncall_reminder"
},
{
"id": 21831,
"mode": "email",
"only_if_involved": null,
"roles": [
"primary",
"secondary",
"shadow",
"manager"
],
"team": "team-foo",
"time_before": 604800,
"type": "oncall_reminder"
}
]
'''
query = '''SELECT `team`.`name` AS `team`, `role`.`name` AS `role`, `contact_mode`.`name` AS `mode`,
`notification_type`.`name` AS `type`, `notification_setting`.`time_before`,
`notification_setting`.`only_if_involved`, `notification_setting`.`id`
@@ -44,6 +93,69 @@ def on_get(req, resp, user_name):
@login_required
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)

View File

@@ -8,7 +8,27 @@ from falcon import HTTPNotFound
def on_get(req, resp, user_name):
"""
Get active teams by user name
Get active teams by user name. Note that this does not return any deleted teams that
this user is a member of.
**Example request**:
.. sourcecode:: http
GET /api/v0/users/jdoe/teams HTTP/1.1
Host: example.com
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Content-Type: application/json
[
"team-foo",
"team-bar"
]
"""
connection = db.connect()
cursor = connection.cursor()

View File

@@ -111,7 +111,59 @@ def get_user_data(fields, filter_params, dbinfo=None):
def on_get(req, resp):
"""
Search users
Get users filtered by params. Returns a list of user info objects for all users matching
filter parameters.
:query id: id of the user
:query id__eq: id of the user
:query id__gt: id greater than
:query id__ge: id greater than or equal
:query id__lt: id less than
:query id__le: id less than or equal
:query name: username
:query name__eq: username
:query name__contains: username contains param
:query name__startswith: username starts with param
:query name__endswith: username ends with param
:query full_name: full name
:query full_name__eq: username
:query full_name__contains: full name contains param
:query full_name__startswith: full name starts with param
:query full_name__endswith: full name ends with param
:query active: whether user has been deactivated (deleted)
**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
[
{
"active": 1,
"contacts": {
"call": "+1 111-111-1111",
"email": "jdoe@example.com",
"im": "jdoe",
"sms": "+1 111-111-1111"
},
"full_name": "John Doe",
"id": 1234,
"name": "jdoe",
"photo_url": "image.example.com",
"time_zone": "US/Pacific"
}
]
"""
resp.body = json_dumps(get_user_data(req.get_param_as_list('fields'), req.params))
@@ -119,7 +171,7 @@ def on_get(req, resp):
@auth.debug_only
def on_post(req, resp):
"""
Create user
Create user. Currently used only in debug mode.
"""
data = load_json_body(req)
connection = db.connect()