mirror of
https://github.com/hanzoai/sentinel.git
synced 2026-09-29 22:34:33 +00:00
192 lines
6.5 KiB
Python
Executable File
192 lines
6.5 KiB
Python
Executable File
#!/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=<<table border="0" cellpadding="2" cellspacing="0" cellborder="0">
|
|
<tr><td align="right" port="i1">HybridCloudForeignKey</td></tr>
|
|
<tr><td align="right" port="i2">HybridCloudForeignKey (nullable)</td></tr>
|
|
<tr><td align="right" port="i3">Explicit ForeignKey</td></tr>
|
|
<tr><td align="right" port="i4">Explicit ForeignKey (nullable)</td></tr>
|
|
<tr><td align="right" port="i5">Implicit ForeignKey</td></tr>
|
|
<tr><td align="right" port="i6">Implicit ForeignKey (nullable)</td></tr>
|
|
<tr><td align="right" port="i7">Control Silo Model</td></tr>
|
|
<tr><td align="right" port="i8">Control Silo Model (dangling)</td></tr>
|
|
<tr><td align="right" port="i9">Region Silo Model</td></tr>
|
|
<tr><td align="right" port="i10">Region Silo Model (dangling)</td></tr>
|
|
<tr><td align="right" port="i11">Unexported Model</td></tr>
|
|
</table>>]
|
|
key2 [label=<<table border="0" cellpadding="2" cellspacing="0" cellborder="0">
|
|
<tr><td port="i1"> </td></tr>
|
|
<tr><td port="i2"> </td></tr>
|
|
<tr><td port="i3"> </td></tr>
|
|
<tr><td port="i4"> </td></tr>
|
|
<tr><td port="i5"> </td></tr>
|
|
<tr><td port="i6"> </td></tr>
|
|
<tr><td port="i7" bgcolor="#ffb6c1ff"> </td></tr>
|
|
<tr><td port="i8" bgcolor="#ffb6c166"> </td></tr>
|
|
<tr><td port="i9" bgcolor="#add8e6ff"> </td></tr>
|
|
<tr><td port="i10" bgcolor="#add8e666"> </td></tr>
|
|
<tr><td port="i11" bgcolor="#c0c0c0ff"> </td></tr>
|
|
</table>>]
|
|
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()
|