#!/usr/bin/env python from __future__ import annotations from collections.abc import Iterable from sentry.backup.scopes import RelocationScope from sentry.runner import configure from sentry.silo.base import SiloMode configure() from enum import Enum, unique from string import Template import click from django.db import models from sentry.backup.dependencies import ForeignField, ForeignFieldKind, ModelRelations, dependencies digraph = Template( """ digraph Models { ranksep = 8; rankdir=LR node [style="rounded,filled",shape="rectangle"]; subgraph cluster_legend { label = "Legend"; fontsize="40" node [shape="plaintext",style="none"] key1 [label=<
HybridCloudForeignKey
HybridCloudForeignKey (nullable)
Explicit ForeignKey
Explicit ForeignKey (nullable)
Implicit ForeignKey
Implicit ForeignKey (nullable)
Control Silo Model
Control Silo Model (dangling)
Region Silo Model
Region Silo Model (dangling)
Unexported Model
>] key2 [label=<
 
 
 
 
 
 
 
 
 
 
 
>] key1:i1:e -> key2:i1:w [color="#008b00ff",style=solid] key1:i2:e -> key2:i2:w [color="#008b0066",style=dashed] key1:i3:e -> key2:i3:w [color="#0000eeff",style=solid] key1:i4:e -> key2:i4:w [color="#0000ee66",style=dashed] key1:i5:e -> key2:i5:w [color="#cd0000ff",style=solid] key1:i6:e -> key2:i6:w [color="#cd000066",style=dashed] } $clusters $edges } """ ) cluster = Template( """ subgraph cluster_$num { label="$name Relocation Scope" style="rounded,filled" shape="rectangle" fillcolor="$fill" fontsize="40" color="#c0c0c0" $nodes } """ ) @unique class ClusterColor(Enum): Purple = "#fff0f5" # lavenderblush Yellow = "#f0e68c" # khaki Green = "#f0fff0" # honeydew Blue = "#cae1ff" # lightsteelblue1 @unique class NodeColor(Enum): Red = "#ffb6c1" # lightpink Blue = "#add8e6" # lightblue @unique class EdgeColor(Enum): Hybrid = "#008b00" # green4 Explicit = "#0000ee" # blue2 Implicit = "#cd0000" # red3 def print_model_node(mr: ModelRelations, silo: SiloMode) -> str: id = mr.model.__name__ color = NodeColor.Red if silo == SiloMode.CONTROL else NodeColor.Blue opacity = "66" if mr.dangling else "ff" return f""""{id}" [fillcolor="{color.value}{opacity}",color="#000000{opacity}"];""" def print_rel_scope_subgraph( name: str, num: int, rels: Iterable[ModelRelations], color: ClusterColor ) -> str: return cluster.substitute( num=num, name=name, fill=color.value, nodes="\n ".join([print_model_node(mr, mr.silos[0]) for mr in rels]), ) def print_edges(mr: ModelRelations) -> str: if len(mr.foreign_keys) == 0: return "" src = mr.model return "\n ".join([print_edge(src, ff.model, ff) for ff in mr.foreign_keys.values()]) def print_edge(src: models.base.ModelBase, dest: models.base.ModelBase, field: ForeignField) -> str: color = EdgeColor.Explicit if field.kind == ForeignFieldKind.HybridCloudForeignKey: color = EdgeColor.Hybrid elif field.kind == ForeignFieldKind.ImplicitForeignKey: color = EdgeColor.Implicit style = "dashed" if field.nullable else "solid" return f""""{src.__name__}":e -> "{dest.__name__}":w [color="{color.value}",style={style}];""" def get_most_permissive_relocation_scope(mr: ModelRelations) -> RelocationScope: if isinstance(mr.relocation_scope, set): return sorted(list(mr.relocation_scope), key=lambda obj: obj.value * -1)[0] return mr.relocation_scope @click.command() @click.option("--show-excluded", default=False, is_flag=True, help="Show unexportable models too") def main(show_excluded: bool): """Generate a graphviz spec for the current model dependency graph.""" # Get all dependencies, filtering as necessary. deps = sorted(dependencies().values(), key=lambda mr: mr.model.__name__) if not show_excluded: deps = list(filter(lambda m: m.relocation_scope != RelocationScope.Excluded, deps)) # Group by most permissive region scope. user_scoped = filter( lambda m: get_most_permissive_relocation_scope(m) == RelocationScope.User, deps ) org_scoped = filter( lambda m: get_most_permissive_relocation_scope(m) == RelocationScope.Organization, deps ) config_scoped = filter( lambda m: get_most_permissive_relocation_scope(m) == RelocationScope.Config, deps ) global_scoped = filter( lambda m: get_most_permissive_relocation_scope(m) == RelocationScope.Global, deps ) # Print nodes. clusters = "".join( [ print_rel_scope_subgraph("User", 1, user_scoped, ClusterColor.Green), print_rel_scope_subgraph("Organization", 2, org_scoped, ClusterColor.Purple), print_rel_scope_subgraph("Config", 3, config_scoped, ClusterColor.Blue), print_rel_scope_subgraph("Global", 4, global_scoped, ClusterColor.Yellow), ] ) # Print edges. edges = "\n ".join(filter(lambda s: s, [print_edges(mr) for mr in deps])) click.echo(digraph.substitute(clusters=clusters, edges=edges)) if __name__ == "__main__": main()