# What this PR does Refactors the PagerDuty migration script to be a bit more generic + adds a migration script to migrate from Splunk OnCall (VictorOps) tldr; ```bash ❯ docker build -t oncall-migrator . [+] Building 0.4s (10/10) FINISHED ❯ docker run --rm \ -e MIGRATING_FROM="pagerduty" \ -e MODE="plan" \ -e ONCALL_API_URL="http://localhost:8080" \ -e ONCALL_API_TOKEN="<ONCALL_API_TOKEN>" \ -e PAGERDUTY_API_TOKEN="<PAGERDUTY_API_TOKEN>" \ oncall-migrator running pagerduty migration script... ❯ docker run --rm \ -e MIGRATING_FROM="splunk" \ -e MODE="plan" \ -e ONCALL_API_URL="http://localhost:8080" \ -e ONCALL_API_TOKEN="<ONCALL_API_TOKEN>" \ -e SPLUNK_API_ID="<SPLUNK_API_ID>" \ -e SPLUNK_API_KEY="<SPLUNK_API_KEY>" \ oncall-migrator migrating from splunk oncall... ``` https://www.loom.com/share/a855062d436a4ef79f030e22528d8c71 ## Checklist - [x] Unit, integration, and e2e (if applicable) tests updated - [x] Documentation added (or `pr:no public docs` PR label added if not required) - [x] Added the relevant release notes label (see labels prefixed w/ `release:`). These labels dictate how your PR will show up in the autogenerated release notes.
130 lines
4.5 KiB
Python
130 lines
4.5 KiB
Python
import time
|
|
import typing
|
|
|
|
from lib.network import api_call as _api_call
|
|
from lib.splunk import types
|
|
|
|
|
|
class SplunkOnCallAPIClient:
|
|
"""
|
|
https://portal.victorops.com/public/api-docs.html
|
|
"""
|
|
|
|
PUBLIC_API_BASE_URL = "https://api.victorops.com/api-public/"
|
|
|
|
def __init__(self, api_id: str, api_key: str):
|
|
self.api_id = api_id
|
|
self.api_key = api_key
|
|
|
|
def _api_call(
|
|
self,
|
|
method: str,
|
|
path: str,
|
|
response_key: typing.Optional[str] = None,
|
|
**kwargs,
|
|
):
|
|
"""
|
|
According to the docs, most API endpoints may only be called a maximum of 2 times per second
|
|
(hence the built-in `time.sleep`)
|
|
"""
|
|
time.sleep(0.5)
|
|
|
|
response = _api_call(
|
|
method,
|
|
self.PUBLIC_API_BASE_URL,
|
|
path,
|
|
headers={
|
|
"X-VO-Api-Id": self.api_id,
|
|
"X-VO-Api-Key": self.api_key,
|
|
},
|
|
**kwargs,
|
|
)
|
|
|
|
return response.json()[response_key] if response_key else response.json()
|
|
|
|
def fetch_user_paging_policies(
|
|
self, user_id: str
|
|
) -> typing.List[types.SplunkUserPagingPolicy]:
|
|
"""
|
|
https://portal.victorops.com/public/api-docs.html#!/User32Paging32Policies/get_api_public_v1_user_user_policies
|
|
"""
|
|
return self._api_call("GET", f"v1/user/{user_id}/policies", "policies")
|
|
|
|
def fetch_users(
|
|
self, include_paging_policies=True
|
|
) -> typing.List[types.SplunkUserWithPagingPolicies]:
|
|
"""
|
|
https://portal.victorops.com/public/api-docs.html#!/Users/get_api_public_v2_user
|
|
"""
|
|
users: typing.List[types.SplunkUserWithPagingPolicies] = self._api_call(
|
|
"GET", "v2/user", "users"
|
|
)
|
|
|
|
if include_paging_policies:
|
|
for user in users:
|
|
user["pagingPolicies"] = self.fetch_user_paging_policies(
|
|
user["username"]
|
|
)
|
|
|
|
return users
|
|
|
|
def fetch_team_members(self, team_slug: str) -> typing.List[types.SplunkTeamMember]:
|
|
"""
|
|
https://portal.victorops.com/public/api-docs.html#!/Teams/get_api_public_v1_team_team_members
|
|
"""
|
|
return self._api_call("GET", f"v1/team/{team_slug}/members", "members")
|
|
|
|
def fetch_teams(self, include_members=False) -> typing.List[types.SplunkTeam]:
|
|
"""
|
|
https://portal.victorops.com/public/api-docs.html#!/Teams/get_api_public_v1_team
|
|
"""
|
|
teams = self._api_call("GET", "v1/team")
|
|
|
|
if include_members:
|
|
for team in teams:
|
|
team["members"] = self.fetch_team_members(team["slug"])
|
|
|
|
return teams
|
|
|
|
def fetch_rotations(self, team_slug: str) -> typing.List[types.SplunkRotation]:
|
|
"""
|
|
https://portal.victorops.com/public/api-docs.html#!/Rotations/get_api_public_v2_team_team_rotations
|
|
"""
|
|
return self._api_call("GET", f"v2/team/{team_slug}/rotations", "rotations")
|
|
|
|
def fetch_schedules(self) -> typing.List[types.SplunkScheduleWithTeamAndRotations]:
|
|
"""
|
|
Schedules in Splunk must be fetched via a team, there is no
|
|
way to list all schedules
|
|
|
|
https://portal.victorops.com/public/api-docs.html#!/On45call/get_api_public_v2_team_team_oncall_schedule
|
|
"""
|
|
schedules: typing.List[types.SplunkScheduleWithTeamAndRotations] = []
|
|
for team in self.fetch_teams():
|
|
team_slug = team["slug"]
|
|
team_rotations = self.fetch_rotations(team_slug)
|
|
|
|
for team_schedule in self._api_call(
|
|
"GET", f"v2/team/{team_slug}/oncall/schedule", "schedules"
|
|
):
|
|
team_schedule["team"] = team
|
|
team_schedule["rotations"] = team_rotations
|
|
|
|
schedules.append(team_schedule)
|
|
return schedules
|
|
|
|
def fetch_escalation_policy(self, policy_id: str) -> types.SplunkEscalationPolicy:
|
|
"""
|
|
Fetch more detailed info about a specific escalation policy
|
|
https://portal.victorops.com/public/api-docs.html#!/Escalation32Policies/get_api_public_v1_policies_policy
|
|
"""
|
|
return self._api_call("GET", f"v1/policies/{policy_id}")
|
|
|
|
def fetch_escalation_policies(self) -> typing.List[types.SplunkEscalationPolicy]:
|
|
"""
|
|
https://portal.victorops.com/public/api-docs.html#!/Escalation32Policies/get_api_public_v1_policies
|
|
"""
|
|
return [
|
|
self.fetch_escalation_policy(policy["policy"]["slug"])
|
|
for policy in self._api_call("GET", "v1/policies", "policies")
|
|
]
|