model-dependency-graphviz 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160
  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. @unique
  67. class NodeColor(Enum):
  68. Red = "lightpink"
  69. Blue = "lightblue"
  70. @unique
  71. class EdgeStyle(Enum):
  72. Hybrid = "[color=green]"
  73. Explicit = "[color=blue]"
  74. Implicit = "[color=red]"
  75. def print_model_node(model: models.base.ModelBase, silo: SiloMode) -> str:
  76. color = NodeColor.Red if silo == SiloMode.CONTROL else NodeColor.Blue
  77. return f""""{model.__name__}" [fillcolor="{color.value}"];"""
  78. def print_rel_scope_subgraph(
  79. name: str, num: int, rels: Iterable[ModelRelations], color: ClusterColor
  80. ) -> str:
  81. return cluster.substitute(
  82. num=num,
  83. name=name,
  84. fill=color.value,
  85. nodes="\n ".join([print_model_node(mr.model, mr.silos[0]) for mr in rels]),
  86. )
  87. def print_edges(mr: ModelRelations) -> str:
  88. if len(mr.foreign_keys) == 0:
  89. return ""
  90. src = mr.model
  91. return "\n ".join([print_edge(src, ff.model, ff.kind) for ff in mr.foreign_keys.values()])
  92. def print_edge(
  93. src: models.base.ModelBase, dest: models.base.ModelBase, kind: ForeignFieldKind
  94. ) -> str:
  95. style = EdgeStyle.Explicit
  96. if kind == ForeignFieldKind.HybridCloudForeignKey:
  97. style = EdgeStyle.Hybrid
  98. elif kind == ForeignFieldKind.ImplicitForeignKey:
  99. style = EdgeStyle.Implicit
  100. return f""""{src.__name__}":e -> "{dest.__name__}":w {style.value};"""
  101. @click.command()
  102. @click.option("--show-excluded", default=False, is_flag=True, help="Show unexportable models too")
  103. def main(show_excluded: bool):
  104. """Generate a graphviz spec for the current model dependency graph."""
  105. # Get all dependencies, filtering as necessary.
  106. deps = sorted(dependencies().values(), key=lambda mr: mr.model.__name__)
  107. if not show_excluded:
  108. deps = list(filter(lambda m: m.relocation_scope != RelocationScope.Excluded, deps))
  109. # Group by region scope.
  110. user_scoped = filter(lambda m: m.relocation_scope == RelocationScope.User, deps)
  111. org_scoped = filter(lambda m: m.relocation_scope == RelocationScope.Organization, deps)
  112. global_scoped = filter(lambda m: m.relocation_scope == RelocationScope.Global, deps)
  113. # Print nodes.
  114. clusters = "".join(
  115. [
  116. print_rel_scope_subgraph("User", 1, user_scoped, ClusterColor.Green),
  117. print_rel_scope_subgraph("Organization", 2, org_scoped, ClusterColor.Purple),
  118. print_rel_scope_subgraph("Global", 3, global_scoped, ClusterColor.Yellow),
  119. ]
  120. )
  121. # Print edges.
  122. edges = "\n ".join(filter(lambda s: s, [print_edges(mr) for mr in deps]))
  123. click.echo(digraph.substitute(clusters=clusters, edges=edges))
  124. if __name__ == "__main__":
  125. main()