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: I4b899287eaf818137d60cb278db2d86598aa6794changes/13/286913/3
parent
bcbbbc9e2c
commit
a787334443
@ -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')
|
@ -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())
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
Loading…
Reference in New Issue