model-dependency-graphviz 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191
  1. #!/usr/bin/env python
  2. from __future__ import annotations
  3. from collections.abc 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 ForeignField, 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">HybridCloudForeignKey (nullable)</td></tr>
  26. <tr><td align="right" port="i3">Explicit ForeignKey</td></tr>
  27. <tr><td align="right" port="i4">Explicit ForeignKey (nullable)</td></tr>
  28. <tr><td align="right" port="i5">Implicit ForeignKey</td></tr>
  29. <tr><td align="right" port="i6">Implicit ForeignKey (nullable)</td></tr>
  30. <tr><td align="right" port="i7">Control Silo Model</td></tr>
  31. <tr><td align="right" port="i8">Control Silo Model (dangling)</td></tr>
  32. <tr><td align="right" port="i9">Region Silo Model</td></tr>
  33. <tr><td align="right" port="i10">Region Silo Model (dangling)</td></tr>
  34. <tr><td align="right" port="i11">Unexported Model</td></tr>
  35. </table>>]
  36. key2 [label=<<table border="0" cellpadding="2" cellspacing="0" cellborder="0">
  37. <tr><td port="i1">&nbsp;</td></tr>
  38. <tr><td port="i2">&nbsp;</td></tr>
  39. <tr><td port="i3">&nbsp;</td></tr>
  40. <tr><td port="i4">&nbsp;</td></tr>
  41. <tr><td port="i5">&nbsp;</td></tr>
  42. <tr><td port="i6">&nbsp;</td></tr>
  43. <tr><td port="i7" bgcolor="#ffb6c1ff">&nbsp;</td></tr>
  44. <tr><td port="i8" bgcolor="#ffb6c166">&nbsp;</td></tr>
  45. <tr><td port="i9" bgcolor="#add8e6ff">&nbsp;</td></tr>
  46. <tr><td port="i10" bgcolor="#add8e666">&nbsp;</td></tr>
  47. <tr><td port="i11" bgcolor="#c0c0c0ff">&nbsp;</td></tr>
  48. </table>>]
  49. key1:i1:e -> key2:i1:w [color="#008b00ff",style=solid]
  50. key1:i2:e -> key2:i2:w [color="#008b0066",style=dashed]
  51. key1:i3:e -> key2:i3:w [color="#0000eeff",style=solid]
  52. key1:i4:e -> key2:i4:w [color="#0000ee66",style=dashed]
  53. key1:i5:e -> key2:i5:w [color="#cd0000ff",style=solid]
  54. key1:i6:e -> key2:i6:w [color="#cd000066",style=dashed]
  55. }
  56. $clusters
  57. $edges
  58. }
  59. """
  60. )
  61. cluster = Template(
  62. """
  63. subgraph cluster_$num {
  64. label="$name Relocation Scope"
  65. style="rounded,filled"
  66. shape="rectangle"
  67. fillcolor="$fill"
  68. fontsize="40"
  69. color="#c0c0c0"
  70. $nodes
  71. }
  72. """
  73. )
  74. @unique
  75. class ClusterColor(Enum):
  76. Purple = "#fff0f5" # lavenderblush
  77. Yellow = "#f0e68c" # khaki
  78. Green = "#f0fff0" # honeydew
  79. Blue = "#cae1ff" # lightsteelblue1
  80. @unique
  81. class NodeColor(Enum):
  82. Red = "#ffb6c1" # lightpink
  83. Blue = "#add8e6" # lightblue
  84. @unique
  85. class EdgeColor(Enum):
  86. Hybrid = "#008b00" # green4
  87. Explicit = "#0000ee" # blue2
  88. Implicit = "#cd0000" # red3
  89. def print_model_node(mr: ModelRelations, silo: SiloMode) -> str:
  90. id = mr.model.__name__
  91. color = NodeColor.Red if silo == SiloMode.CONTROL else NodeColor.Blue
  92. opacity = "66" if mr.dangling else "ff"
  93. return f""""{id}" [fillcolor="{color.value}{opacity}",color="#000000{opacity}"];"""
  94. def print_rel_scope_subgraph(
  95. name: str, num: int, rels: Iterable[ModelRelations], color: ClusterColor
  96. ) -> str:
  97. return cluster.substitute(
  98. num=num,
  99. name=name,
  100. fill=color.value,
  101. nodes="\n ".join([print_model_node(mr, mr.silos[0]) for mr in rels]),
  102. )
  103. def print_edges(mr: ModelRelations) -> str:
  104. if len(mr.foreign_keys) == 0:
  105. return ""
  106. src = mr.model
  107. return "\n ".join([print_edge(src, ff.model, ff) for ff in mr.foreign_keys.values()])
  108. def print_edge(src: models.base.ModelBase, dest: models.base.ModelBase, field: ForeignField) -> str:
  109. color = EdgeColor.Explicit
  110. if field.kind == ForeignFieldKind.HybridCloudForeignKey:
  111. color = EdgeColor.Hybrid
  112. elif field.kind == ForeignFieldKind.ImplicitForeignKey:
  113. color = EdgeColor.Implicit
  114. style = "dashed" if field.nullable else "solid"
  115. return f""""{src.__name__}":e -> "{dest.__name__}":w [color="{color.value}",style={style}];"""
  116. def get_most_permissive_relocation_scope(mr: ModelRelations) -> RelocationScope:
  117. if isinstance(mr.relocation_scope, set):
  118. return sorted(list(mr.relocation_scope), key=lambda obj: obj.value * -1)[0]
  119. return mr.relocation_scope
  120. @click.command()
  121. @click.option("--show-excluded", default=False, is_flag=True, help="Show unexportable models too")
  122. def main(show_excluded: bool):
  123. """Generate a graphviz spec for the current model dependency graph."""
  124. # Get all dependencies, filtering as necessary.
  125. deps = sorted(dependencies().values(), key=lambda mr: mr.model.__name__)
  126. if not show_excluded:
  127. deps = list(filter(lambda m: m.relocation_scope != RelocationScope.Excluded, deps))
  128. # Group by most permissive region scope.
  129. user_scoped = filter(
  130. lambda m: get_most_permissive_relocation_scope(m) == RelocationScope.User, deps
  131. )
  132. org_scoped = filter(
  133. lambda m: get_most_permissive_relocation_scope(m) == RelocationScope.Organization, deps
  134. )
  135. config_scoped = filter(
  136. lambda m: get_most_permissive_relocation_scope(m) == RelocationScope.Config, deps
  137. )
  138. global_scoped = filter(
  139. lambda m: get_most_permissive_relocation_scope(m) == RelocationScope.Global, deps
  140. )
  141. # Print nodes.
  142. clusters = "".join(
  143. [
  144. print_rel_scope_subgraph("User", 1, user_scoped, ClusterColor.Green),
  145. print_rel_scope_subgraph("Organization", 2, org_scoped, ClusterColor.Purple),
  146. print_rel_scope_subgraph("Config", 3, config_scoped, ClusterColor.Blue),
  147. print_rel_scope_subgraph("Global", 4, global_scoped, ClusterColor.Yellow),
  148. ]
  149. )
  150. # Print edges.
  151. edges = "\n ".join(filter(lambda s: s, [print_edges(mr) for mr in deps]))
  152. click.echo(digraph.substitute(clusters=clusters, edges=edges))
  153. if __name__ == "__main__":
  154. main()