oncall-engine/dev/scripts/generate-fake-data/main.py

306 lines
9.2 KiB
Python
Raw Normal View History

fake-data generation script + fixes for django-silk and django-debug-toolbar (#1128) # What this PR does ## Main stuff - add Python script to populate local Grafana/OnCall setup w/ large amounts of fake data. Right now the data types that can be generated are: - teams and Admin users via the Grafana API (must be synced manually by going into the UI before going onto the next step) - Calendar Schedules which have three 8h oncall-shifts, via the OnCall public API - fixes `django-debug-toolbar` when being run in `docker-compose` locally ## Other stuff - documents how to easily modify the Grafana `docker-compose` container provisioning configuration - document solutions for two backend setup related issues encountered when running the engine/celery workers locally, outside of `docker-compose`, on an Apple silicon Mac - fixes small bug in `grafana_plugin.helpers.client.APIClient.call_api` where it would call `response.json()` for all requests, regardless of whether or not the response actually contained data or not - in `engine/settings/dev.py`, properly setup `django-silk` and document the steps to use it locally - make it possible to log out debug SQL queries by specifying `DEV_DEBUG_VIEW_SQL_QUERIES` env var, rather than having to uncomment out a section of `settings/dev.py` ## Which issue(s) this PR fixes - Some local setup issues when trying to use `django-silk` and `django-debug-toolbar` - Makes it much easier to populate your local setup with a lot of fake data - Makes it possible to easily modify your local grafana's provisioning configuration ## Checklist - [ ] Tests updated (N/A) - [ ] Documentation added (N/A) - [ ] `CHANGELOG.md` updated (N/A)
2023-01-20 09:19:41 +01:00
import argparse
import asyncio
import math
import random
import typing
import uuid
from datetime import datetime
import aiohttp
from faker import Faker
from tqdm.asyncio import tqdm
fake = Faker()
TEAMS_USERS_COMMAND = "generate_teams_and_users"
SCHEDULES_ONCALL_SHIFTS_COMMAND = "generate_schedules_and_oncall_shifts"
GRAFANA_API_URL = None
ONCALL_API_URL = None
ONCALL_API_TOKEN = None
class OnCallApiUser(typing.TypedDict):
id: str
class OnCallApiOnCallShift(typing.TypedDict):
id: str
class OnCallApiListUsersResponse(typing.TypedDict):
results: typing.List[OnCallApiUser]
class GrafanaAPIUser(typing.TypedDict):
id: int
def _generate_unique_email() -> str:
user = fake.profile()
return f'{uuid.uuid4()}-{user["mail"]}'
async def _grafana_api_request(
http_session: aiohttp.ClientSession, method: str, url: str, **request_kwargs
) -> typing.Awaitable[typing.Dict]:
resp = await http_session.request(
method, f"{GRAFANA_API_URL}{url}", **request_kwargs
)
return await resp.json()
async def _oncall_api_request(
http_session: aiohttp.ClientSession, method: str, url: str, **request_kwargs
) -> typing.Awaitable[typing.Dict]:
resp = await http_session.request(
method,
f"{ONCALL_API_URL}{url}",
headers={"Authorization": ONCALL_API_TOKEN},
**request_kwargs,
)
return await resp.json()
def generate_team(
http_session: aiohttp.ClientSession, org_id: int
) -> typing.Callable[[], typing.Awaitable[typing.Dict]]:
"""
https://grafana.com/docs/grafana/latest/developers/http_api/team/#add-team
"""
def _generate_team() -> typing.Awaitable[typing.Dict]:
return _grafana_api_request(
http_session,
"POST",
"/api/teams",
json={
"name": str(uuid.uuid4()),
"email": _generate_unique_email(),
"orgId": org_id,
},
)
return _generate_team
def generate_user(
http_session: aiohttp.ClientSession, org_id: int
) -> typing.Callable[[], typing.Awaitable[typing.Dict]]:
"""
https://grafana.com/docs/grafana/latest/developers/http_api/admin/#global-users
"""
async def _generate_user() -> typing.Awaitable[typing.Dict]:
user = fake.profile()
# create the user in grafana
grafana_user: GrafanaAPIUser = await _grafana_api_request(
http_session,
"POST",
"/api/admin/users",
json={
"name": user["name"],
"email": _generate_unique_email(),
"login": str(uuid.uuid4()),
"password": fake.password(length=20),
"OrgId": org_id,
},
)
# update the user's basic role in grafana to Admin
# https://grafana.com/docs/grafana/latest/developers/http_api/org/#updates-the-given-user
await _grafana_api_request(
http_session,
"PATCH",
f'/api/org/users/{grafana_user["id"]}',
json={"role": "Admin"},
)
return grafana_user
return _generate_user
def generate_schedule(
http_session: aiohttp.ClientSession, oncall_shift_ids: typing.List[str]
) -> typing.Callable[[], typing.Awaitable[typing.Dict]]:
def _generate_schedule() -> typing.Awaitable[typing.Dict]:
# Create a schedule
# https://grafana.com/docs/oncall/latest/oncall-api-reference/schedules/#create-a-schedule
return _oncall_api_request(
http_session,
"POST",
"/api/v1/schedules",
json={
"name": f"Schedule {uuid.uuid4()}",
"type": "calendar",
"time_zone": "UTC",
"shifts": oncall_shift_ids,
},
)
return _generate_schedule
def _bulk_generate_data(
iterations: int,
data_generator_func: typing.Callable[[], typing.Awaitable[typing.Dict]],
) -> typing.Awaitable[typing.List[typing.Dict]]:
return tqdm.gather(
*[asyncio.ensure_future(data_generator_func()) for _ in range(iterations)]
)
async def _generate_grafana_teams_and_users(
args: argparse.Namespace, http_session: aiohttp.ClientSession
) -> typing.Awaitable[None]:
global GRAFANA_API_URL
GRAFANA_API_URL = args.grafana_api_url
org_id = args.grafana_org_id
print("Generating team(s)")
await _bulk_generate_data(args.teams, generate_team(http_session, org_id))
print("Generating user(s)")
await _bulk_generate_data(args.users, generate_user(http_session, org_id))
print(
f"""
Grafana teams and users generated
Now head to the OnCall plugin and manually visit the plugin to trigger a sync. This will sync grafana
teams/users to OnCall. Once completed, you can run the {SCHEDULES_ONCALL_SHIFTS_COMMAND} command.
"""
)
async def _generate_oncall_schedules_and_oncall_shifts(
args: argparse.Namespace, http_session: aiohttp.ClientSession
) -> typing.Awaitable[None]:
global ONCALL_API_URL, ONCALL_API_TOKEN
ONCALL_API_URL = args.oncall_api_url
ONCALL_API_TOKEN = args.oncall_api_token
today = datetime.now()
print("Fetching users from OnCall API")
# Fetch users from the OnCall API
users: OnCallApiListUsersResponse = await _oncall_api_request(
http_session, "GET", "/api/v1/users"
)
user_ids: typing.List[str] = [u["id"] for u in users["results"]]
num_users = len(user_ids)
print(f"Fetched {num_users} user(s) from the OnCall API")
async def _create_oncall_shift(shift_start_time: str) -> typing.Awaitable[str]:
"""
Creates an eight hour shift.
`shift_start_time` - ex. 09:00:00, 15:00:00
https://grafana.com/docs/oncall/latest/oncall-api-reference/on_call_shifts/#create-an-oncall-shift
"""
new_shift: OnCallApiOnCallShift = await _oncall_api_request(
http_session,
"POST",
"/api/v1/on_call_shifts",
json={
"name": f"On call shift{uuid.uuid4()}",
"type": "rolling_users",
"start": today.strftime(f"%Y-%m-%dT{shift_start_time}"),
"time_zone": "UTC",
"duration": 60 * 60 * 8, # 8 hours
"frequency": "daily",
"week_start": "MO",
"rolling_users": [
[u] for u in random.choices(user_ids, k=math.floor(num_users / 2))
],
"start_rotation_from_user_index": 0,
"team_id": None,
},
)
oncall_shift_id = new_shift["id"]
print(f"Generated OnCall shift w/ ID {oncall_shift_id}")
return oncall_shift_id
print("Creating three 8h on-call shifts")
morning_shift_id = await _create_oncall_shift("00:00:00")
afternoon_shift_id = await _create_oncall_shift("08:00:00")
evening_shift_id = await _create_oncall_shift("16:00:00")
print("Generating schedules(s)")
await _bulk_generate_data(
args.schedules,
generate_schedule(
http_session, [morning_shift_id, afternoon_shift_id, evening_shift_id]
),
)
async def main() -> typing.Awaitable[None]:
parser = argparse.ArgumentParser(
description="Set of commands to help generate fake data in a Grafana OnCall setup."
)
subparsers = parser.add_subparsers(help="sub-command help")
grafana_command_parser = subparsers.add_parser(
TEAMS_USERS_COMMAND,
description="Command to generate teams and users in Grafana",
)
grafana_command_parser.set_defaults(func=_generate_grafana_teams_and_users)
grafana_command_parser.add_argument(
"--grafana-api-url",
help="Grafana API URL. This should include the basic authentication username/password in the URL. ex. http://oncall:oncall@localhost:3000",
default="http://oncall:oncall@localhost:3000",
)
grafana_command_parser.add_argument(
"--grafana-org-id",
help="Org ID, in Grafana, of the org that you would like to generate data for",
type=int,
default=1,
)
grafana_command_parser.add_argument(
"-t", "--teams", help="Number of teams to generate", default=10, type=int
)
grafana_command_parser.add_argument(
"-u", "--users", help="Number of users to generate", default=1_000, type=int
)
oncall_command_parser = subparsers.add_parser(
SCHEDULES_ONCALL_SHIFTS_COMMAND,
description="Command to generate schedules and on-call shifts in OnCall",
)
oncall_command_parser.set_defaults(
func=_generate_oncall_schedules_and_oncall_shifts
)
oncall_command_parser.add_argument(
"--oncall-api-url",
help="OnCall API URL",
default="http://localhost:8080",
)
oncall_command_parser.add_argument(
"--oncall-api-token", help="OnCall API token", required=True
)
oncall_command_parser.add_argument(
"-s",
"--schedules",
help="Number of schedules to generate",
default=100,
type=int,
)
args = parser.parse_args()
async with aiohttp.ClientSession(
connector=aiohttp.TCPConnector(limit=5)
) as session:
await args.func(args, session)
if __name__ == "__main__":
asyncio.run(main())