diff --git a/CHANGELOG.md b/CHANGELOG.md index 82d88b7..b891c89 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,16 @@ the unreleased section to the section for the new release. No unreleased changes. +### Added + +- Adds support for generating a Graphviz diagram of an Organization with the new + `OrganizationDataBuilder.to_dot()` function +- Adds `DOT` as a supported output format for the `organization dump-all` command + +### Changed + +- breaking: Renames `organization dump-json` CLI command to `organization dump-all` + ## [0.1.0-beta2] - 2021-06-16 ### Added diff --git a/aws_data_tools/builders/organizations.py b/aws_data_tools/builders/organizations.py index b6aabf5..bd95e66 100644 --- a/aws_data_tools/builders/organizations.py +++ b/aws_data_tools/builders/organizations.py @@ -5,6 +5,8 @@ from dataclasses import dataclass, field, InitVar from typing import Any, Dict, List, Union +from graphviz import Digraph, unflatten + from ..client import APIClient from ..models.base import ModelBase from ..utils import query_tags @@ -78,6 +80,34 @@ def api(self, func: str, **kwargs) -> Union[List[Dict[str, Any]], Dict[str, Any] self.Connect() return self.client.api(func, **kwargs) + def to_dot(self) -> str: + """Return the organization as a GraphViz DOT diagram""" + graph = Digraph("Organization", filename="organization.dot") + nodes = [] + nodes.append(self.dm.root) + nodes.extend(self.dm.organizational_units) + nodes.extend(self.dm.accounts) + for node in nodes: + if getattr(node, "parent", None) is None: + continue + shape = None + if isinstance(node, Root): + shape = "circle" + elif isinstance(node, OrganizationalUnit): + shape = "box" + elif isinstance(node, Account): + shape = "ellipse" + else: + continue + graph.node(node.id, label=node.name, shape=shape) + graph.edge(node.parent.id, node.id) + return unflatten( + graph.source, + stagger=10, + fanout=10, + chain=10, + ) + def __e_organization(self) -> Dict[str, str]: """Extract org description data from the DescribeOrganization API""" return self.api("describe_organization").get("organization") diff --git a/aws_data_tools/cli/__init__.py b/aws_data_tools/cli/__init__.py index 5652bce..8d5a539 100644 --- a/aws_data_tools/cli/__init__.py +++ b/aws_data_tools/cli/__init__.py @@ -64,6 +64,14 @@ def handle_error(ctx, err_msg, tb=None): @organization.command(short_help="Dump org data as JSON") +@option( + "--format", + "-f", + "format_", + default="JSON", + type=Choice(["DOT", "JSON", "YAML"], case_sensitive=False), + help="The output format for the data", +) @option( "--no-accounts", default=False, @@ -76,24 +84,16 @@ def handle_error(ctx, err_msg, tb=None): is_flag=True, help="Exclude policy data from the model", ) -@option( - "--format", - "-f", - "format_", - default="JSON", - type=Choice(["JSON", "YAML"], case_sensitive=False), - help="The output format for the data", -) @option("--out-file", "-o", help="File path to write data instead of stdout") @pass_context -def dump_json( +def dump_all( ctx: Dict[str, Any], + format_: str, no_accounts: bool, no_policies: bool, - format_: str, out_file: str, ) -> None: - """Dump a JSON representation of the organization""" + """Dump a data representation of the organization""" err_msg = None tb = None try: @@ -114,6 +114,8 @@ def dump_json( s_func = odb.to_json elif format_ == "YAML": s_func = odb.to_yaml + elif format_ == "DOT": + s_func = odb.to_dot if out_file is None: out_file = "-" with open_file(out_file, mode="wb") as f: diff --git a/poetry.lock b/poetry.lock index 9e8bc0e..5d6b747 100644 --- a/poetry.lock +++ b/poetry.lock @@ -349,6 +349,19 @@ python-versions = ">=3.5" [package.dependencies] gitdb = ">=4.0.1,<5" +[[package]] +name = "graphviz" +version = "0.16" +description = "Simple Python interface for Graphviz" +category = "main" +optional = false +python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*" + +[package.extras] +dev = ["tox (>=3)", "flake8", "pep8-naming", "wheel", "twine"] +docs = ["sphinx (>=1.8)", "sphinx-rtd-theme"] +test = ["mock (>=3)", "pytest (>=4)", "pytest-mock (>=2)", "pytest-cov"] + [[package]] name = "identify" version = "2.2.10" @@ -1490,7 +1503,7 @@ docs = ["blacken-docs", "mkdocs", "mkdocs-git-revision-date-localized-plugin", " [metadata] lock-version = "1.1" python-versions = ">=3.9,<4" -content-hash = "a37894b4faf7fde9ee2400d2b5244c1986692f43012c93a6ed1ca19ba08bf331" +content-hash = "0db0fa5b821cd863d9e27750d07d92c15daf375494937d2e5c1d75db1a4a0c5a" [metadata.files] appdirs = [ @@ -1677,6 +1690,10 @@ gitpython = [ {file = "GitPython-3.1.17-py3-none-any.whl", hash = "sha256:29fe82050709760081f588dd50ce83504feddbebdc4da6956d02351552b1c135"}, {file = "GitPython-3.1.17.tar.gz", hash = "sha256:ee24bdc93dce357630764db659edaf6b8d664d4ff5447ccfeedd2dc5c253f41e"}, ] +graphviz = [ + {file = "graphviz-0.16-py2.py3-none-any.whl", hash = "sha256:3cad5517c961090dfc679df6402a57de62d97703e2880a1a46147bb0dc1639eb"}, + {file = "graphviz-0.16.zip", hash = "sha256:d2d25af1c199cad567ce4806f0449cb74eb30cf451fd7597251e1da099ac6e57"}, +] identify = [ {file = "identify-2.2.10-py2.py3-none-any.whl", hash = "sha256:18d0c531ee3dbc112fa6181f34faa179de3f57ea57ae2899754f16a7e0ff6421"}, {file = "identify-2.2.10.tar.gz", hash = "sha256:5b41f71471bc738e7b586308c3fca172f78940195cb3bf6734c1e66fdac49306"}, diff --git a/pyproject.toml b/pyproject.toml index abefb6a..0fe2dc5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -48,6 +48,7 @@ mkdocs = {version = "^1.1.2", optional = true, extras = ["docs"]} mkdocs-git-revision-date-localized-plugin = {version = "^0.9.2", optional = true, extras = ["docs"]} mkdocs-macros-plugin = {version = "^0.5.5", optional = true, extras = ["docs"]} mkdocs-material = {version = "^7.1.5", optional = true, extras = ["docs"]} +graphviz = "^0.16" [tool.poetry.dev-dependencies] black = "^21.5b1"