"""
Perform bulk operations on Grafana.
BulkOperations defines the basic interface. It is meant to be parametrised in 2 phases:
- information source: Subclasses of BulkOperations implement methods to get orgs and resources
- actions: Subclasses of those classes implement the actions to take
For example:
BulkGrafanaOperation uses a Grafana instance as its source
BulkExporter uses BulkGrafanaOperations to list all objects and write them to disk
"""
import logging
from abc import ABC, abstractmethod
from functools import lru_cache
from pathlib import Path
from typing import Generator, List, Tuple
from grafana_client import GrafanaApi
from grafanarmadillo.alerter import Alerter
from grafanarmadillo.dashboarder import Dashboarder
from grafanarmadillo.find import Finder
from grafanarmadillo.paths import PathCodec
from grafanarmadillo.templator import Templator
from grafanarmadillo.types import AlertContent, DashboardContent, GrafanaPath, OrgMeta
from grafanarmadillo.util import exactly_one, read_from_file, write_to_file
l = logging.getLogger(__name__)
[docs]@lru_cache
def get_all_orgs(gfn_multiorg) -> List[OrgMeta]:
"""Get a list of all orgs."""
return gfn_multiorg.organizations.list_organization()
[docs]@lru_cache
def get_org(gfn_multiorg, org_name: str):
"""Get an org by name, efficiently."""
return exactly_one(
list(filter(lambda o: o["name"] == org_name, get_all_orgs(gfn_multiorg))),
f"org with name {org_name}"
)
[docs]class BulkOperation(ABC):
"""Run bulk operations on Grafana."""
def __init__(self, cfg: dict):
self.cfg = cfg
self.gfn_multiorg = GrafanaApi(**self.cfg)
[docs] def run(self):
"""Run this bulk operation."""
for org, gfn in self.all_orgs():
for out_path, dashboard_content in self.get_all_dashboards(org, gfn):
self.each_dashboard(out_path, dashboard_content)
for out_path, alert_content in self.get_all_alerts(org, gfn):
self.each_alert(out_path, alert_content)
[docs] @abstractmethod
def all_orgs(self) -> Generator[Tuple[OrgMeta, GrafanaApi], None, None]:
"""
Iterate over all organisations.
This method should be implemented for each source of information.
"""
[docs] @abstractmethod
def get_all_dashboards(self, org: OrgMeta, gfn: GrafanaApi) -> Generator[Tuple[GrafanaPath, DashboardContent], None, None]:
"""
Iterate over all dashboards.
This method should be implemented for each source of information.
"""
[docs] @abstractmethod
def get_all_alerts(self, org: OrgMeta, gfn: GrafanaApi) -> Generator[Tuple[GrafanaPath, AlertContent], None, None]:
"""
Iterate over all alerts.
This method should be implemented for each source of information.
"""
[docs] @abstractmethod
def each_dashboard(self, path: GrafanaPath, dashboard: DashboardContent):
"""
Act on each dashboard in Grafana.
This method should be implemented for each operation.
"""
[docs] @abstractmethod
def each_alert(self, path: GrafanaPath, alert: AlertContent):
"""
Act on each alert in Grafana.
This method should be implemented for each operation.
"""
[docs]class BulkGrafanaOperation(BulkOperation, ABC):
"""Bulk operations which uses a Grafana instance as its source."""
[docs] def all_orgs(self) -> Generator[Tuple[OrgMeta, GrafanaApi], None, None]:
"""Iterate over all organisations in Grafana."""
orgs = self.gfn_multiorg.organizations.list_organization()
for org in orgs:
gfn = GrafanaApi(**{**self.cfg, "organization_id": org["id"]})
yield org, gfn
[docs] def get_all_dashboards(self, org: OrgMeta, gfn: GrafanaApi) -> Generator[Tuple[GrafanaPath, DashboardContent], None, None]:
"""Get all dashboards."""
finder, dashboarder = Finder(gfn), Dashboarder(gfn)
dashboards = finder.list_dashboards()
for dashboard in dashboards:
dashboard_content, folder = dashboarder.export_dashboard(dashboard)
if folder is None:
folder_name = "General"
else:
folder_name = folder["title"]
out_path = GrafanaPath(dashboard_content["title"], folder_name, org["name"])
yield out_path, dashboard_content
[docs] def get_all_alerts(self, org: OrgMeta, gfn: GrafanaApi) -> Generator[Tuple[GrafanaPath, AlertContent], None, None]:
"""Get all alerts."""
finder, alerter = Finder(gfn), Alerter(gfn)
alerts = finder.list_alerts()
for alert in alerts:
alert_content, folder = alerter.export_alert(alert)
folder_name = folder["title"]
out_path = GrafanaPath(alert_content["title"], folder_name, org["name"])
yield out_path, alert_content
[docs]class BulkFileOperation(BulkOperation, ABC):
"""Bulk operation which uses a filetree as its source."""
def __init__(self, cfg: dict, root_directory: Path):
self.root_directory = root_directory
super().__init__(cfg)
[docs] def all_orgs(self) -> Generator[Tuple[OrgMeta, GrafanaApi], None, None]:
"""Iterate over all organisations in Grafana."""
orgs = {PathCodec.decode_segment(o.name) for o in self.root_directory.glob("*/*")}
for org_name in orgs:
org = self.gfn_multiorg.organization.find_organization(org_name)
gfn = GrafanaApi(**{**self.cfg, "organization_id": org["id"]})
yield org, gfn
[docs] def get_all_dashboards(self, org: OrgMeta, gfn: GrafanaApi) -> Generator[Tuple[GrafanaPath, DashboardContent], None, None]:
"""Get all dashboards."""
folders = (self.root_directory / "dashboards" / PathCodec.encode_segment(org["name"])).glob("*")
for folder_path in folders:
for dashboard_path in folder_path.glob("*.json"):
content = read_from_file(dashboard_path)
yield GrafanaPath(PathCodec.decode_segment(dashboard_path.stem), PathCodec.decode_segment(folder_path.name), org["name"]), content
[docs] def get_all_alerts(self, org: OrgMeta, gfn: GrafanaApi) -> Generator[Tuple[GrafanaPath, AlertContent], None, None]:
"""Get all alerts."""
folders = (self.root_directory / "alerts" / PathCodec.encode_segment(org["name"])).glob("*")
for folder_path in folders:
for alert_path in folder_path.glob("*.json"):
content = read_from_file(alert_path)
yield GrafanaPath(PathCodec.decode_segment(alert_path.stem), PathCodec.decode_segment(folder_path.name), org["name"]), content
[docs]class BulkExporter(BulkGrafanaOperation):
"""Export all resources from Grafana to files."""
def __init__(self, cfg: dict, root_directory: Path, templator: Templator):
self.root_directory = root_directory
self.templator = templator
super().__init__(cfg)
[docs] def each_dashboard(self, path: GrafanaPath, dashboard: DashboardContent):
"""Write each dashboard to files."""
dashboard_templated = self.templator.make_template_from_dashboard(dashboard)
l.info(f"export dashboard path={path}")
write_to_file((self.root_directory / "dashboards" / PathCodec.encode_grafana(path)).with_suffix(".json"), dashboard_templated)
[docs] def each_alert(self, path: GrafanaPath, alert: AlertContent):
"""Write each alert to files."""
alert_templated = self.templator.make_template_from_dashboard(alert)
l.info(f"export alert path={path}")
write_to_file((self.root_directory / "alerts" / PathCodec.encode_grafana(path)).with_suffix(".json"), alert_templated)
[docs]class BulkImporter(BulkFileOperation):
"""Import all resources from files into Grafana."""
def __init__(self, cfg: dict, root_directory: Path, templator: Templator):
self.templator = templator
super().__init__(cfg, root_directory)
[docs] def each_dashboard(self, path: GrafanaPath, dashboard: DashboardContent):
"""Import each dashboard into Grafana."""
org = get_org(self.gfn_multiorg, path.org)
gfn = GrafanaApi(**{**self.cfg, "organization_id": org["id"]})
finder, dashboarder = Finder(gfn), Dashboarder(gfn)
folder = finder.create_or_get_folder(path.folder)
dashboard_templated = self.templator.make_dashboard_from_template(
dashboard, dashboard
)
l.info(f"import dashboard path={path}")
dashboarder.import_dashboard(dashboard_templated, folder)
[docs] def each_alert(self, path: GrafanaPath, alert: AlertContent):
"""Import each alert into Grafana."""
org = get_org(self.gfn_multiorg, path.org)
gfn = GrafanaApi(**{**self.cfg, "organization_id": org["id"]})
finder, alerter = Finder(gfn), Alerter(gfn)
alert_info, folder_info = finder.create_or_get_alert(path)
alert_templated = self.templator.make_dashboard_from_template(alert_info, alert)
l.info(f"import alert path={path}")
alerter.import_alert(alert_templated, folder_info)