model-dependency-graphviz 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176
  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 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">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 EdgeColor(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) for ff in mr.foreign_keys.values()])
  93. def print_edge(src: models.base.ModelBase, dest: models.base.ModelBase, field: ForeignField) -> str:
  94. color = EdgeColor.Explicit
  95. if field.kind == ForeignFieldKind.HybridCloudForeignKey:
  96. color = EdgeColor.Hybrid
  97. elif field.kind == ForeignFieldKind.ImplicitForeignKey:
  98. color = EdgeColor.Implicit
  99. style = "dashed" if field.nullable else "solid"
  100. return f""""{src.__name__}":e -> "{dest.__name__}":w [{color.value},style={style}];"""
  101. def get_most_permissive_relocation_scope(mr: ModelRelations) -> RelocationScope:
  102. if isinstance(mr.relocation_scope, set):
  103. return sorted(list(mr.relocation_scope), key=lambda obj: obj.value * -1)[0]
  104. return mr.relocation_scope
  105. @click.command()
  106. @click.option("--show-excluded", default=False, is_flag=True, help="Show unexportable models too")
  107. def main(show_excluded: bool):
  108. """Generate a graphviz spec for the current model dependency graph."""
  109. # Get all dependencies, filtering as necessary.
  110. deps = sorted(dependencies().values(), key=lambda mr: mr.model.__name__)
  111. if not show_excluded:
  112. deps = list(filter(lambda m: m.relocation_scope != RelocationScope.Excluded, deps))
  113. # Group by most permissive region scope.
  114. user_scoped = filter(
  115. lambda m: get_most_permissive_relocation_scope(m) == RelocationScope.User, deps
  116. )
  117. org_scoped = filter(
  118. lambda m: get_most_permissive_relocation_scope(m) == RelocationScope.Organization, deps
  119. )
  120. config_scoped = filter(
  121. lambda m: get_most_permissive_relocation_scope(m) == RelocationScope.Config, deps
  122. )
  123. global_scoped = filter(
  124. lambda m: get_most_permissive_relocation_scope(m) == RelocationScope.Global, deps
  125. )
  126. # Print nodes.
  127. clusters = "".join(
  128. [
  129. print_rel_scope_subgraph("User", 1, user_scoped, ClusterColor.Green),
  130. print_rel_scope_subgraph("Organization", 2, org_scoped, ClusterColor.Purple),
  131. print_rel_scope_subgraph("Config", 3, config_scoped, ClusterColor.Blue),
  132. print_rel_scope_subgraph("Global", 4, global_scoped, ClusterColor.Yellow),
  133. ]
  134. )
  135. # Print edges.
  136. edges = "\n ".join(filter(lambda s: s, [print_edges(mr) for mr in deps]))
  137. click.echo(digraph.substitute(clusters=clusters, edges=edges))
  138. if __name__ == "__main__":
  139. main()