summaryrefslogtreecommitdiff
path: root/heatclient
diff options
context:
space:
mode:
authorSteve Baker <sbaker@redhat.com>2016-03-02 09:30:36 +1300
committerSteve Baker <sbaker@redhat.com>2016-05-31 09:15:03 +1200
commita78733444337c7f2b0b4eb506a2605ea05c149a3 (patch)
tree2e09fb7d6b292546a96cdee979272f4edabb8a89 /heatclient
parentbcbbbc9e2c88065ecb9d77ab53fdb8d5e207ff97 (diff)
downloadpython-heatclient-a78733444337c7f2b0b4eb506a2605ea05c149a3.tar.gz
A resource list formatter for graphviz dot diagrams
This change registers a "dot" formatter for the stack resource list command. It generates the text for a graphviz dot diagram which can be rendered as an image with this example usage: openstack stack resource list --format dot -n3 my_stack | dot -Tsvg -o my_stack.svg Nested resources are fully supported by creating nodes for top-level resources then linking stack resources to a subgraph representing the nested stack. Change-Id: I4b899287eaf818137d60cb278db2d86598aa6794
Diffstat (limited to 'heatclient')
-rw-r--r--heatclient/common/resource_formatter.py150
-rw-r--r--heatclient/osc/v1/resource.py7
-rw-r--r--heatclient/tests/unit/test_resource_formatter.py100
-rw-r--r--heatclient/tests/unit/var/dot_test.json204
4 files changed, 461 insertions, 0 deletions
diff --git a/heatclient/common/resource_formatter.py b/heatclient/common/resource_formatter.py
new file mode 100644
index 0000000..7a5ec7f
--- /dev/null
+++ b/heatclient/common/resource_formatter.py
@@ -0,0 +1,150 @@
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+#
+
+import collections
+import hashlib
+
+from cliff.formatters import base
+
+
+class ResourceDotInfo(object):
+
+ def __init__(self, res):
+ self.resource = res
+ links = {l['rel']: l['href'] for l in res.links}
+ self.nested_dot_id = self.dot_id(links.get('nested'), 'stack')
+ self.stack_dot_id = self.dot_id(links.get('stack'), 'stack')
+ self.res_dot_id = self.dot_id(links.get('self'))
+
+ @staticmethod
+ def dot_id(url, prefix=None):
+ """Build an id with a prefix and a truncated hash of the URL"""
+ if not url:
+ return None
+ if not prefix:
+ prefix = 'r'
+ hash_object = hashlib.sha256(url.encode('utf-8'))
+ return '%s_%s' % (prefix, hash_object.hexdigest()[:20])
+
+
+class ResourceDotFormatter(base.ListFormatter):
+ def add_argument_group(self, parser):
+ pass
+
+ def emit_list(self, column_names, data, stdout, parsed_args):
+ writer = ResourceDotWriter(data, stdout)
+ writer.write()
+
+
+class ResourceDotWriter(object):
+
+ def __init__(self, data, stdout):
+ self.resources_by_stack = collections.defaultdict(
+ collections.OrderedDict)
+ self.resources_by_dot_id = collections.OrderedDict()
+ self.nested_stack_ids = []
+ self.stdout = stdout
+
+ for r in data:
+ rinfo = ResourceDotInfo(r)
+ if rinfo.stack_dot_id:
+ self.resources_by_stack[
+ rinfo.stack_dot_id][r.resource_name] = rinfo
+ if rinfo.res_dot_id:
+ self.resources_by_dot_id[rinfo.res_dot_id] = rinfo
+ if rinfo.nested_dot_id:
+ self.nested_stack_ids.append(rinfo.nested_dot_id)
+
+ def write(self):
+ stdout = self.stdout
+
+ stdout.write('digraph G {\n')
+ stdout.write(' graph [\n'
+ ' fontsize=10 fontname="Verdana" '
+ 'compound=true rankdir=LR\n'
+ ' ]\n')
+
+ self.write_root_nodes()
+ self.write_subgraphs()
+ self.write_nested_stack_edges()
+ self.write_required_by_edges()
+ stdout.write('}\n')
+
+ def write_root_nodes(self):
+ for stack_dot_id in set(self.resources_by_stack.keys()).difference(
+ self.nested_stack_ids):
+ resources = self.resources_by_stack[stack_dot_id]
+ self.write_nodes(resources, 2)
+
+ def write_subgraphs(self):
+ for dot_id, rinfo in self.resources_by_dot_id.items():
+ if rinfo.nested_dot_id:
+ resources = self.resources_by_stack[rinfo.nested_dot_id]
+ if resources:
+ self.write_subgraph(resources, rinfo)
+
+ def write_nodes(self, resources, indent):
+ stdout = self.stdout
+ spaces = ' ' * indent
+ for rinfo in resources.values():
+ r = rinfo.resource
+ dot_id = rinfo.res_dot_id
+ if r.resource_status.endswith('FAILED'):
+ style = 'style=filled color=red'
+ else:
+ style = ''
+ stdout.write('%s%s [label="%s\n%s" %s];\n'
+ % (spaces, dot_id, r.resource_name,
+ r.resource_type, style))
+ stdout.write('\n')
+
+ def write_subgraph(self, resources, nested_resource):
+ stdout = self.stdout
+ stack_dot_id = nested_resource.nested_dot_id
+ nested_name = nested_resource.resource.resource_name
+ stdout.write(' subgraph cluster_%s {\n' % stack_dot_id)
+ stdout.write(' label="%s";\n' % nested_name)
+ self.write_nodes(resources, 4)
+ stdout.write(' }\n\n')
+
+ def write_required_by_edges(self):
+ stdout = self.stdout
+ for dot_id, rinfo in self.resources_by_dot_id.items():
+ r = rinfo.resource
+
+ required_by = r.required_by
+ stack_dot_id = rinfo.stack_dot_id
+ if not required_by or not stack_dot_id:
+ continue
+
+ stack_resources = self.resources_by_stack.get(stack_dot_id, {})
+ for req in required_by:
+ other_rinfo = stack_resources.get(req)
+ if other_rinfo:
+ stdout.write(' %s -> %s;\n'
+ % (rinfo.res_dot_id, other_rinfo.res_dot_id))
+ stdout.write('\n')
+
+ def write_nested_stack_edges(self):
+ stdout = self.stdout
+ for dot_id, rinfo in self.resources_by_dot_id.items():
+ if rinfo.nested_dot_id:
+ nested_resources = self.resources_by_stack[rinfo.nested_dot_id]
+ if nested_resources:
+ first_resource = list(nested_resources.values())[0]
+ stdout.write(
+ ' %s -> %s [\n color=dimgray lhead=cluster_%s '
+ 'arrowhead=none\n ];\n'
+ % (dot_id, first_resource.res_dot_id,
+ rinfo.nested_dot_id))
+ stdout.write('\n')
diff --git a/heatclient/osc/v1/resource.py b/heatclient/osc/v1/resource.py
index 056fbb1..54f9302 100644
--- a/heatclient/osc/v1/resource.py
+++ b/heatclient/osc/v1/resource.py
@@ -78,6 +78,10 @@ class ResourceList(lister.Lister):
log = logging.getLogger(__name__ + '.ResourceListStack')
+ @property
+ def formatter_namespace(self):
+ return 'heatclient.resource.formatter.list'
+
def get_parser(self, prog_name):
parser = super(ResourceList, self).get_parser(prog_name)
parser.add_argument(
@@ -126,6 +130,9 @@ class ResourceList(lister.Lister):
msg = _('Stack not found: %s') % parsed_args.stack
raise exc.CommandError(msg)
+ if parsed_args.formatter == 'dot':
+ return [], resources
+
columns = ['physical_resource_id', 'resource_type', 'resource_status',
'updated_time']
diff --git a/heatclient/tests/unit/test_resource_formatter.py b/heatclient/tests/unit/test_resource_formatter.py
new file mode 100644
index 0000000..e3a053d
--- /dev/null
+++ b/heatclient/tests/unit/test_resource_formatter.py
@@ -0,0 +1,100 @@
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+#
+
+import json
+import os
+import six
+
+from heatclient.common import resource_formatter
+from heatclient.osc.v1 import resource
+from heatclient.tests.unit.osc.v1 import fakes as orchestration_fakes
+from heatclient.v1 import resources as v1_resources
+
+
+TEST_VAR_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__),
+ 'var'))
+
+
+class TestStackResourceListDotFormat(orchestration_fakes.TestOrchestrationv1):
+
+ response_path = os.path.join(TEST_VAR_DIR, 'dot_test.json')
+
+ data = '''digraph G {
+ graph [
+ fontsize=10 fontname="Verdana" compound=true rankdir=LR
+ ]
+ r_f34a35baf594b319a741 [label="rg1
+OS::Heat::ResourceGroup" ];
+ r_121e343b017a6d246f36 [label="random2
+OS::Heat::RandomString" ];
+ r_dbcae38ad41dc991751d [label="random1
+OS::Heat::RandomString" style=filled color=red];
+
+ subgraph cluster_stack_16437984473ec64a8e6c {
+ label="rg1";
+ r_30e9aa76bc0d53310cde [label="1
+OS::Heat::ResourceGroup" ];
+ r_63c05d424cb708f1599f [label="0
+OS::Heat::ResourceGroup" ];
+
+ }
+
+ subgraph cluster_stack_fbfb461c8cc84b686c08 {
+ label="1";
+ r_e2e5c36ae18e29d9c299 [label="1
+OS::Heat::RandomString" ];
+ r_56c62630a0d655bce234 [label="0
+OS::Heat::RandomString" ];
+
+ }
+
+ subgraph cluster_stack_d427657dfccc28a131a7 {
+ label="0";
+ r_240756913e2e940387ff [label="1
+OS::Heat::RandomString" ];
+ r_81c64c43d9131aceedbb [label="0
+OS::Heat::RandomString" ];
+
+ }
+
+ r_f34a35baf594b319a741 -> r_30e9aa76bc0d53310cde [
+ color=dimgray lhead=cluster_stack_16437984473ec64a8e6c arrowhead=none
+ ];
+ r_30e9aa76bc0d53310cde -> r_e2e5c36ae18e29d9c299 [
+ color=dimgray lhead=cluster_stack_fbfb461c8cc84b686c08 arrowhead=none
+ ];
+ r_63c05d424cb708f1599f -> r_240756913e2e940387ff [
+ color=dimgray lhead=cluster_stack_d427657dfccc28a131a7 arrowhead=none
+ ];
+
+ r_dbcae38ad41dc991751d -> r_121e343b017a6d246f36;
+
+}
+'''
+
+ def setUp(self):
+ super(TestStackResourceListDotFormat, self).setUp()
+ self.resource_client = self.app.client_manager.orchestration.resources
+ self.cmd = resource.ResourceList(self.app, None)
+ with open(self.response_path) as f:
+ response = json.load(f)
+ self.resources = []
+ for r in response['resources']:
+ self.resources.append(v1_resources.Resource(None, r))
+
+ def test_resource_list(self):
+ out = six.StringIO()
+ formatter = resource_formatter.ResourceDotFormatter()
+ formatter.emit_list(None, self.resources, out, None)
+
+ self.assertEqual(self.data, out.getvalue())
diff --git a/heatclient/tests/unit/var/dot_test.json b/heatclient/tests/unit/var/dot_test.json
new file mode 100644
index 0000000..816d8f5
--- /dev/null
+++ b/heatclient/tests/unit/var/dot_test.json
@@ -0,0 +1,204 @@
+{
+ "resources": [
+ {
+ "links": [
+ {
+ "href": "http://192.0.2.1:8004/v1/e00f891d1aec4e6194b9164dd71c68f1/stacks/dot_test/cea9d5e4-03b9-42ed-9d2a-c7cea6cd75d7/resources/rg1",
+ "rel": "self"
+ },
+ {
+ "href": "http://192.0.2.1:8004/v1/e00f891d1aec4e6194b9164dd71c68f1/stacks/dot_test/cea9d5e4-03b9-42ed-9d2a-c7cea6cd75d7",
+ "rel": "stack"
+ },
+ {
+ "href": "http://192.0.2.1:8004/v1/e00f891d1aec4e6194b9164dd71c68f1/stacks/dot_test-rg1-fhwpldmwgi53/07e7dd08-efe9-4f52-a5c0-9dc29d9ccd39",
+ "rel": "nested"
+ }
+ ],
+ "logical_resource_id": "rg1",
+ "physical_resource_id": "07e7dd08-efe9-4f52-a5c0-9dc29d9ccd39",
+ "required_by": [],
+ "resource_name": "rg1",
+ "resource_status": "CREATE_COMPLETE",
+ "resource_status_reason": "state changed",
+ "resource_type": "OS::Heat::ResourceGroup",
+ "updated_time": "2016-05-26T02:51:13Z"
+ },
+ {
+ "links": [
+ {
+ "href": "http://192.0.2.1:8004/v1/e00f891d1aec4e6194b9164dd71c68f1/stacks/dot_test-rg1-fhwpldmwgi53/07e7dd08-efe9-4f52-a5c0-9dc29d9ccd39/resources/1",
+ "rel": "self"
+ },
+ {
+ "href": "http://192.0.2.1:8004/v1/e00f891d1aec4e6194b9164dd71c68f1/stacks/dot_test-rg1-fhwpldmwgi53/07e7dd08-efe9-4f52-a5c0-9dc29d9ccd39",
+ "rel": "stack"
+ },
+ {
+ "href": "http://192.0.2.1:8004/v1/e00f891d1aec4e6194b9164dd71c68f1/stacks/dot_test-rg1-fhwpldmwgi53-1-omdjadmwtu2c/105a252b-17d7-4bda-b47c-ad33ef113ce2",
+ "rel": "nested"
+ }
+ ],
+ "logical_resource_id": "1",
+ "parent_resource": "rg1",
+ "physical_resource_id": "105a252b-17d7-4bda-b47c-ad33ef113ce2",
+ "required_by": [],
+ "resource_name": "1",
+ "resource_status": "CREATE_COMPLETE",
+ "resource_status_reason": "state changed",
+ "resource_type": "OS::Heat::ResourceGroup",
+ "updated_time": "2016-05-26T02:51:13Z"
+ },
+ {
+ "links": [
+ {
+ "href": "http://192.0.2.1:8004/v1/e00f891d1aec4e6194b9164dd71c68f1/stacks/dot_test-rg1-fhwpldmwgi53-1-omdjadmwtu2c/105a252b-17d7-4bda-b47c-ad33ef113ce2/resources/1",
+ "rel": "self"
+ },
+ {
+ "href": "http://192.0.2.1:8004/v1/e00f891d1aec4e6194b9164dd71c68f1/stacks/dot_test-rg1-fhwpldmwgi53-1-omdjadmwtu2c/105a252b-17d7-4bda-b47c-ad33ef113ce2",
+ "rel": "stack"
+ }
+ ],
+ "logical_resource_id": "1",
+ "parent_resource": "1",
+ "physical_resource_id": "dot_test-rg1-fhwpldmwgi53-1-omdjadmwtu2c-1-uiq2mqi3wxvi",
+ "required_by": [],
+ "resource_name": "1",
+ "resource_status": "CREATE_COMPLETE",
+ "resource_status_reason": "state changed",
+ "resource_type": "OS::Heat::RandomString",
+ "updated_time": "2016-05-26T02:51:13Z"
+ },
+ {
+ "links": [
+ {
+ "href": "http://192.0.2.1:8004/v1/e00f891d1aec4e6194b9164dd71c68f1/stacks/dot_test-rg1-fhwpldmwgi53-1-omdjadmwtu2c/105a252b-17d7-4bda-b47c-ad33ef113ce2/resources/0",
+ "rel": "self"
+ },
+ {
+ "href": "http://192.0.2.1:8004/v1/e00f891d1aec4e6194b9164dd71c68f1/stacks/dot_test-rg1-fhwpldmwgi53-1-omdjadmwtu2c/105a252b-17d7-4bda-b47c-ad33ef113ce2",
+ "rel": "stack"
+ }
+ ],
+ "logical_resource_id": "0",
+ "parent_resource": "1",
+ "physical_resource_id": "dot_test-rg1-fhwpldmwgi53-1-omdjadmwtu2c-0-3x7zr6jblmev",
+ "required_by": [],
+ "resource_name": "0",
+ "resource_status": "CREATE_COMPLETE",
+ "resource_status_reason": "state changed",
+ "resource_type": "OS::Heat::RandomString",
+ "updated_time": "2016-05-26T02:51:13Z"
+ },
+ {
+ "links": [
+ {
+ "href": "http://192.0.2.1:8004/v1/e00f891d1aec4e6194b9164dd71c68f1/stacks/dot_test-rg1-fhwpldmwgi53/07e7dd08-efe9-4f52-a5c0-9dc29d9ccd39/resources/0",
+ "rel": "self"
+ },
+ {
+ "href": "http://192.0.2.1:8004/v1/e00f891d1aec4e6194b9164dd71c68f1/stacks/dot_test-rg1-fhwpldmwgi53/07e7dd08-efe9-4f52-a5c0-9dc29d9ccd39",
+ "rel": "stack"
+ },
+ {
+ "href": "http://192.0.2.1:8004/v1/e00f891d1aec4e6194b9164dd71c68f1/stacks/dot_test-rg1-fhwpldmwgi53-0-fbof4yqwgqij/62a26d5f-73d6-43f1-946d-0a7c1b93f761",
+ "rel": "nested"
+ }
+ ],
+ "logical_resource_id": "0",
+ "parent_resource": "rg1",
+ "physical_resource_id": "62a26d5f-73d6-43f1-946d-0a7c1b93f761",
+ "required_by": [],
+ "resource_name": "0",
+ "resource_status": "CREATE_COMPLETE",
+ "resource_status_reason": "state changed",
+ "resource_type": "OS::Heat::ResourceGroup",
+ "updated_time": "2016-05-26T02:51:13Z"
+ },
+ {
+ "links": [
+ {
+ "href": "http://192.0.2.1:8004/v1/e00f891d1aec4e6194b9164dd71c68f1/stacks/dot_test-rg1-fhwpldmwgi53-0-fbof4yqwgqij/62a26d5f-73d6-43f1-946d-0a7c1b93f761/resources/1",
+ "rel": "self"
+ },
+ {
+ "href": "http://192.0.2.1:8004/v1/e00f891d1aec4e6194b9164dd71c68f1/stacks/dot_test-rg1-fhwpldmwgi53-0-fbof4yqwgqij/62a26d5f-73d6-43f1-946d-0a7c1b93f761",
+ "rel": "stack"
+ }
+ ],
+ "logical_resource_id": "1",
+ "parent_resource": "0",
+ "physical_resource_id": "dot_test-rg1-fhwpldmwgi53-0-fbof4yqwgqij-1-kww7wmvmoawr",
+ "required_by": [],
+ "resource_name": "1",
+ "resource_status": "CREATE_COMPLETE",
+ "resource_status_reason": "state changed",
+ "resource_type": "OS::Heat::RandomString",
+ "updated_time": "2016-05-26T02:51:13Z"
+ },
+ {
+ "links": [
+ {
+ "href": "http://192.0.2.1:8004/v1/e00f891d1aec4e6194b9164dd71c68f1/stacks/dot_test-rg1-fhwpldmwgi53-0-fbof4yqwgqij/62a26d5f-73d6-43f1-946d-0a7c1b93f761/resources/0",
+ "rel": "self"
+ },
+ {
+ "href": "http://192.0.2.1:8004/v1/e00f891d1aec4e6194b9164dd71c68f1/stacks/dot_test-rg1-fhwpldmwgi53-0-fbof4yqwgqij/62a26d5f-73d6-43f1-946d-0a7c1b93f761",
+ "rel": "stack"
+ }
+ ],
+ "logical_resource_id": "0",
+ "parent_resource": "0",
+ "physical_resource_id": "dot_test-rg1-fhwpldmwgi53-0-fbof4yqwgqij-0-4xpmeguv6zt4",
+ "required_by": [],
+ "resource_name": "0",
+ "resource_status": "CREATE_COMPLETE",
+ "resource_status_reason": "state changed",
+ "resource_type": "OS::Heat::RandomString",
+ "updated_time": "2016-05-26T02:51:13Z"
+ },
+ {
+ "links": [
+ {
+ "href": "http://192.0.2.1:8004/v1/e00f891d1aec4e6194b9164dd71c68f1/stacks/dot_test/cea9d5e4-03b9-42ed-9d2a-c7cea6cd75d7/resources/random2",
+ "rel": "self"
+ },
+ {
+ "href": "http://192.0.2.1:8004/v1/e00f891d1aec4e6194b9164dd71c68f1/stacks/dot_test/cea9d5e4-03b9-42ed-9d2a-c7cea6cd75d7",
+ "rel": "stack"
+ }
+ ],
+ "logical_resource_id": "random2",
+ "physical_resource_id": "dot_test-random2-23dvgoy3niw2",
+ "required_by": [],
+ "resource_name": "random2",
+ "resource_status": "CREATE_COMPLETE",
+ "resource_status_reason": "state changed",
+ "resource_type": "OS::Heat::RandomString",
+ "updated_time": "2016-05-26T02:51:13Z"
+ },
+ {
+ "links": [
+ {
+ "href": "http://192.0.2.1:8004/v1/e00f891d1aec4e6194b9164dd71c68f1/stacks/dot_test/cea9d5e4-03b9-42ed-9d2a-c7cea6cd75d7/resources/random1",
+ "rel": "self"
+ },
+ {
+ "href": "http://192.0.2.1:8004/v1/e00f891d1aec4e6194b9164dd71c68f1/stacks/dot_test/cea9d5e4-03b9-42ed-9d2a-c7cea6cd75d7",
+ "rel": "stack"
+ }
+ ],
+ "logical_resource_id": "random1",
+ "physical_resource_id": "dot_test-random1-naw5hspxwnef",
+ "required_by": [
+ "random2"
+ ],
+ "resource_name": "random1",
+ "resource_status": "CREATE_FAILED",
+ "resource_status_reason": "state changed",
+ "resource_type": "OS::Heat::RandomString",
+ "updated_time": "2016-05-26T02:51:13Z"
+ }
+ ]
+}