model-dependency-graphviz 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177
  1. #!/usr/bin/env python
  2. from __future__ import annotations
  3. from typing import Iterable
  4. from sentry.backup.scopes import RelocationScope
  5. from sentry.runner import configure
  6. from sentry.silo.base import SiloMode
  7. configure()
  8. from enum import Enum, unique
  9. from string import Template
  10. import click
  11. from django.db import models
  12. from sentry.backup.dependencies import ForeignFieldKind, ModelRelations, dependencies
  13. digraph = Template(
  14. """
  15. digraph Models {
  16. ranksep = 8;
  17. rankdir=LR
  18. node [style="rounded,filled",shape="rectangle"];
  19. subgraph cluster_legend {
  20. label = "Legend";
  21. fontsize="40"
  22. node [shape="plaintext",style="none"]
  23. key1 [label=<<table border="0" cellpadding="2" cellspacing="0" cellborder="0">
  24. <tr><td align="right" port="i1">HybridCloudForeignKey</td></tr>
  25. <tr><td align="right" port="i2">Explicit ForeignKey</td></tr>
  26. <tr><td align="right" port="i3">Implicit ForeignKey</td></tr>
  27. <tr><td align="right" port="i4">Control Silo Model</td></tr>
  28. <tr><td align="right" port="i5">Region Silo Model</td></tr>
  29. <tr><td align="right" port="i6">Unexported Model</td></tr>
  30. </table>>]
  31. key2 [label=<<table border="0" cellpadding="2" cellspacing="0" cellborder="0">
  32. <tr><td port="i1">&nbsp;</td></tr>
  33. <tr><td port="i2">&nbsp;</td></tr>
  34. <tr><td port="i3">&nbsp;</td></tr>
  35. <tr><td port="i4" bgcolor="lightcoral">&nbsp;</td></tr>
  36. <tr><td port="i5" bgcolor="lightblue">&nbsp;</td></tr>
  37. <tr><td port="i6" bgcolor="grey">&nbsp;</td></tr>
  38. </table>>]
  39. key1:i1:e -> key2:i1:w [color=green]
  40. key1:i2:e -> key2:i2:w [color=blue]
  41. key1:i3:e -> key2:i3:w [color=red]
  42. }
  43. $clusters
  44. $edges
  45. }
  46. """
  47. )
  48. cluster = Template(
  49. """
  50. subgraph cluster_$num {
  51. label="$name Relocation Scope"
  52. style="rounded,filled"
  53. shape="rectangle"
  54. fillcolor="$fill"
  55. fontsize="40"
  56. color="grey"
  57. $nodes
  58. }
  59. """
  60. )
  61. @unique
  62. class ClusterColor(Enum):
  63. Purple = "lavenderblush"
  64. Yellow = "khaki"
  65. Green = "honeydew"
  66. Blue = "lightsteelblue1"
  67. @unique
  68. class NodeColor(Enum):
  69. Red = "lightpink"
  70. Blue = "lightblue"
  71. @unique
  72. class EdgeStyle(Enum):
  73. Hybrid = "[color=green]"
  74. Explicit = "[color=blue]"
  75. Implicit = "[color=red]"
  76. def print_model_node(model: models.base.ModelBase, silo: SiloMode) -> str:
  77. color = NodeColor.Red if silo == SiloMode.CONTROL else NodeColor.Blue
  78. return f""""{model.__name__}" [fillcolor="{color.value}"];"""
  79. def print_rel_scope_subgraph(
  80. name: str, num: int, rels: Iterable[ModelRelations], color: ClusterColor
  81. ) -> str:
  82. return cluster.substitute(
  83. num=num,
  84. name=name,
  85. fill=color.value,
  86. nodes="\n ".join([print_model_node(mr.model, mr.silos[0]) for mr in rels]),
  87. )
  88. def print_edges(mr: ModelRelations) -> str:
  89. if len(mr.foreign_keys) == 0:
  90. return ""
  91. src = mr.model
  92. return "\n ".join([print_edge(src, ff.model, ff.kind) for ff in mr.foreign_keys.values()])
  93. def print_edge(
  94. src: models.base.ModelBase, dest: models.base.ModelBase, kind: ForeignFieldKind
  95. ) -> str:
  96. style = EdgeStyle.Explicit
  97. if kind == ForeignFieldKind.HybridCloudForeignKey:
  98. style = EdgeStyle.Hybrid
  99. elif kind == ForeignFieldKind.ImplicitForeignKey:
  100. style = EdgeStyle.Implicit
  101. return f""""{src.__name__}":e -> "{dest.__name__}":w {style.value};"""
  102. def get_most_permissive_relocation_scope(mr: ModelRelations) -> RelocationScope:
  103. if isinstance(mr.relocation_scope, set):
  104. return sorted(list(mr.relocation_scope), key=lambda obj: obj.value * -1)[0]
  105. return mr.relocation_scope
  106. @click.command()
  107. @click.option("--show-excluded", default=False, is_flag=True, help="Show unexportable models too")
  108. def main(show_excluded: bool):
  109. """Generate a graphviz spec for the current model dependency graph."""
  110. # Get all dependencies, filtering as necessary.
  111. deps = sorted(dependencies().values(), key=lambda mr: mr.model.__name__)
  112. if not show_excluded:
  113. deps = list(filter(lambda m: m.relocation_scope != RelocationScope.Excluded, deps))
  114. # Group by most permissive region scope.
  115. user_scoped = filter(
  116. lambda m: get_most_permissive_relocation_scope(m) == RelocationScope.User, deps
  117. )
  118. org_scoped = filter(
  119. lambda m: get_most_permissive_relocation_scope(m) == RelocationScope.Organization, deps
  120. )
  121. config_scoped = filter(
  122. lambda m: get_most_permissive_relocation_scope(m) == RelocationScope.Config, deps
  123. )
  124. global_scoped = filter(
  125. lambda m: get_most_permissive_relocation_scope(m) == RelocationScope.Global, deps
  126. )
  127. # Print nodes.
  128. clusters = "".join(
  129. [
  130. print_rel_scope_subgraph("User", 1, user_scoped, ClusterColor.Green),
  131. print_rel_scope_subgraph("Organization", 2, org_scoped, ClusterColor.Purple),
  132. print_rel_scope_subgraph("Config", 3, config_scoped, ClusterColor.Blue),
  133. print_rel_scope_subgraph("Global", 4, global_scoped, ClusterColor.Yellow),
  134. ]
  135. )
  136. # Print edges.
  137. edges = "\n ".join(filter(lambda s: s, [print_edges(mr) for mr in deps]))
  138. click.echo(digraph.substitute(clusters=clusters, edges=edges))
  139. if __name__ == "__main__":
  140. main()