oncall-engine/tools/migrators/lib/grafana/service_migrate.py
Bob Cotton 0e1dcd2e71
Service to service model migration (#5485)
# What this PR does

Adds Service and Business Service migration to the Pager Duty Migrator.

To test, in addition to the OnCall configs, you need to crate a Grafana
Service Account with `Admin` permission and generate a token. You will
set `GRAFANA_SERVICE_ACCOUNT_URL`, per the README, to
`https://<namespace>:<token>@<server>` The namespace is the stack id, in
the format of `stacks-<stack id>`

Service migration is configurable, filterable, and idempotent.

## Which issue(s) this PR closes

Related to [issue link here]

<!--
*Note*: If you want the issue to be auto-closed once the PR is merged,
change "Related to" to "Closes" in the line above.
If you have more than one GitHub issue that this PR closes, be sure to
preface
each issue link with a [closing
keyword](https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/using-keywords-in-issues-and-pull-requests#linking-a-pull-request-to-an-issue).
This ensures that the issue(s) are auto-closed once the PR has been
merged.
-->

## Checklist

- [x] Unit, integration, and e2e (if applicable) tests updated
- [x] Documentation added (or `pr:no public docs` PR label added if not
required)
- [ ] 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.

---------

Co-authored-by: Joey Orlando <joey.orlando@grafana.com>
Co-authored-by: GitHub Actions <actions@github.com>
Co-authored-by: grafana-irm-app[bot] <165293418+grafana-irm-app[bot]@users.noreply.github.com>
Co-authored-by: Joey Orlando <joseph.t.orlando@gmail.com>
2025-03-15 21:07:59 -04:00

242 lines
8 KiB
Python

"""
Migration logic for converting PagerDuty services to Grafana's service model.
This module provides functions to migrate PagerDuty services to Grafana's service model,
including creating the required 'pagerduty' Group and handling both individual and batch migrations.
"""
import json
import logging
from typing import Any, Dict, List, Optional
from lib.common.report import TAB
from lib.grafana.service_model_client import ServiceModelClient
from lib.grafana.transform import transform_service, validate_component
from lib.pagerduty.report import format_service
from lib.pagerduty.resources.business_service import BusinessService
from lib.pagerduty.resources.services import TechnicalService
# Configure logging
logging.basicConfig(
level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
)
logger = logging.getLogger(__name__)
def migrate_technical_service(
client: ServiceModelClient, service: TechnicalService, dry_run: bool = False
) -> Optional[Dict[str, Any]]:
"""
Migrate a single technical service to Grafana's service model.
Args:
client: The ServiceModelClient to use
service: The technical service to migrate
dry_run: If True, only validate and log what would be done
Returns:
The created component if successful, None otherwise
"""
try:
# Transform the service
component = transform_service(service)
# Check if component already exists
existing = client.get_component(component["metadata"]["name"])
if existing:
print(TAB + format_service(service, True) + " (preserved)")
service.preserved = True
service.migration_errors = None
return existing
# Validate the transformed component
errors = validate_component(component)
if errors:
service.migration_errors = errors
service.preserved = False
print(TAB + format_service(service, False))
return None
if dry_run:
service.migration_errors = None
service.preserved = False
print(TAB + format_service(service, True) + " (would create)")
return component
# Create the component
created = client.create_component(component)
service.migration_errors = None
service.preserved = False
print(TAB + format_service(service, True) + " (created)")
return created
except Exception as e:
service.migration_errors = str(e)
service.preserved = False
print(TAB + format_service(service, False))
return None
def migrate_business_service(
client: ServiceModelClient, service: BusinessService, dry_run: bool = False
) -> Optional[Dict[str, Any]]:
"""
Migrate a single business service to Grafana's service model.
Args:
client: The ServiceModelClient to use
service: The business service to migrate
dry_run: If True, only validate and log what would be done
Returns:
The created component if successful, None otherwise
"""
try:
# Transform the service
component = transform_service(service)
# Check if component already exists
existing = client.get_component(component["metadata"]["name"])
if existing:
print(TAB + format_service(service, True) + " (preserved)")
service.preserved = True
service.migration_errors = None
return existing
# Validate the transformed component
errors = validate_component(component)
if errors:
service.migration_errors = errors
service.preserved = False
print(TAB + format_service(service, False))
return None
if dry_run:
service.migration_errors = None
service.preserved = False
print(TAB + format_service(service, True) + " (would create)")
return component
# Create the component
created = client.create_component(component)
service.migration_errors = None
service.preserved = False
print(TAB + format_service(service, True) + " (created)")
return created
except Exception as e:
service.migration_errors = str(e)
service.preserved = False
print(TAB + format_service(service, False))
return None
def _migrate_service_batch(
client: ServiceModelClient,
services: List[Any],
migrate_func: callable,
dry_run: bool = False,
) -> Dict[str, Any]:
"""
Migrate a batch of services using the provided migration function.
Args:
client: The ServiceModelClient to use
services: List of services to migrate
migrate_func: Function to use for migrating each service
dry_run: If True, only validate and log what would be done
Returns:
Dictionary containing migration statistics and created components
"""
created_components = {}
for service in services:
component = migrate_func(client, service, dry_run)
if component:
created_components[service.id] = component
return created_components
def _update_service_dependencies(
client: ServiceModelClient,
services: List[Any],
created_components: Dict[str, Any],
dry_run: bool = False,
) -> None:
"""
Update dependencies for all services with proper refs.
Args:
client: The ServiceModelClient to use
services: List of services to update
created_components: Dictionary of created components by service ID
dry_run: If True, only validate and log what would be done
"""
for service in services:
if service.id in created_components and service.dependencies:
component_name = created_components[service.id]["metadata"]["name"]
depends_on_refs = [
{
"apiVersion": "servicemodel.ext.grafana.com/v1alpha1",
"kind": "Component",
"name": created_components[dep.id]["metadata"]["name"],
}
for dep in service.dependencies
if dep.id in created_components
]
if depends_on_refs:
# Create patch payload with only the dependsOnRefs field
patch_payload = {"spec": {"dependsOnRefs": depends_on_refs}}
if not dry_run:
try:
client.patch_component(component_name, patch_payload)
print(f"Updated dependencies for service: {service.name}")
except Exception as e:
print(
f"Failed to update dependencies for service {service.name}: {e}"
)
# Log the full error details for debugging
print(f"Patch payload: {json.dumps(patch_payload, indent=2)}")
def migrate_all_services(
client: ServiceModelClient,
technical_services: List[TechnicalService],
business_services: List[BusinessService],
dry_run: bool = False,
) -> None:
"""
Migrate all PagerDuty services to Grafana's service model.
Args:
client: The ServiceModelClient to use
technical_services: List of technical services to migrate
business_services: List of business services to migrate
dry_run: If True, only validate and log what would be done
Returns:
Dictionary containing migration statistics
"""
# Migrate technical services
tech_components = _migrate_service_batch(
client, technical_services, migrate_technical_service, dry_run
)
# Migrate business services
bus_components = _migrate_service_batch(
client, business_services, migrate_business_service, dry_run
)
# Update dependencies
created_components = {**tech_components, **bus_components}
_update_service_dependencies(
client, technical_services + business_services, created_components, dry_run
)
return