support dot format
you can view the graph in dot format instead of json using the "-f dot" in cli command for topology and rca dot format is very common for visualization of graphs there are many tools that support this format including online visualizers Story: 2004063 Task: 27070 Change-Id: I44779d5d46a6ca279e2766b0d3f8e7ca02706b84
This commit is contained in:
@@ -51,3 +51,5 @@ testtools==2.2.0
|
|||||||
traceback2==1.4.0
|
traceback2==1.4.0
|
||||||
unittest2==1.1.0
|
unittest2==1.1.0
|
||||||
wrapt==1.7.0
|
wrapt==1.7.0
|
||||||
|
pydot==1.4.1
|
||||||
|
networkx==2.0
|
||||||
|
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
features:
|
||||||
|
- Topology and Rca now can be printed in dot format using
|
||||||
|
the CLI with ``-f dot``
|
@@ -9,3 +9,5 @@ osc-lib>=1.8.0 # Apache-2.0
|
|||||||
oslo.utils>=3.33.0 # Apache-2.0
|
oslo.utils>=3.33.0 # Apache-2.0
|
||||||
keystoneauth1>=3.4.0 # Apache-2.0
|
keystoneauth1>=3.4.0 # Apache-2.0
|
||||||
iso8601>=0.1.11 # MIT
|
iso8601>=0.1.11 # MIT
|
||||||
|
networkx>=2.0 # BSD
|
||||||
|
pydot>=1.4.1 # BSD
|
||||||
|
@@ -62,6 +62,14 @@ openstack.rca.v1 =
|
|||||||
rca_webhook_list = vitrageclient.v1.cli.webhook:WebhookList
|
rca_webhook_list = vitrageclient.v1.cli.webhook:WebhookList
|
||||||
rca_webhook_show = vitrageclient.v1.cli.webhook:WebhookShow
|
rca_webhook_show = vitrageclient.v1.cli.webhook:WebhookShow
|
||||||
|
|
||||||
|
vitrageclient.formatter.show =
|
||||||
|
dot = vitrageclient.common.formatters:DOTFormatter
|
||||||
|
json = cliff.formatters.json_format:JSONFormatter
|
||||||
|
shell = cliff.formatters.shell:ShellFormatter
|
||||||
|
table = cliff.formatters.table:TableFormatter
|
||||||
|
value = cliff.formatters.value:ValueFormatter
|
||||||
|
yaml = cliff.formatters.yaml_format:YAMLFormatter
|
||||||
|
|
||||||
[build_sphinx]
|
[build_sphinx]
|
||||||
source-dir = doc/source
|
source-dir = doc/source
|
||||||
build-dir = doc/build
|
build-dir = doc/build
|
||||||
|
57
vitrageclient/common/formatters.py
Normal file
57
vitrageclient/common/formatters.py
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
# Copyright 2018 - Nokia Corporation
|
||||||
|
# #
|
||||||
|
# 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.
|
||||||
|
from cliff.formatters import base
|
||||||
|
|
||||||
|
from networkx.drawing.nx_pydot import write_dot
|
||||||
|
from networkx.readwrite import json_graph
|
||||||
|
|
||||||
|
import networkx as nx
|
||||||
|
|
||||||
|
|
||||||
|
class DOTFormatter(base.SingleFormatter):
|
||||||
|
def add_argument_group(self, parser):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def emit_one(self, column_names, data, stdout, parsed_args):
|
||||||
|
data = {n: i for n, i in zip(column_names, data)}
|
||||||
|
|
||||||
|
# pydot doesn't like the name property
|
||||||
|
# use label instead
|
||||||
|
self._relabel(data)
|
||||||
|
|
||||||
|
if nx.__version__ >= '2.0':
|
||||||
|
graph = json_graph.node_link_graph(
|
||||||
|
data, attrs={'name': 'graph_index'})
|
||||||
|
else:
|
||||||
|
graph = json_graph.node_link_graph(data)
|
||||||
|
|
||||||
|
write_dot(graph, stdout)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _relabel(data):
|
||||||
|
for node in data['nodes']:
|
||||||
|
name = node.pop('name', None)
|
||||||
|
v_type = node['vitrage_type']
|
||||||
|
if name and name != v_type:
|
||||||
|
# if name and type the same
|
||||||
|
# dont print twice its redundant
|
||||||
|
# e.g openstack.cluster
|
||||||
|
node[u'label'] = name + '\n' + v_type
|
||||||
|
else:
|
||||||
|
node[u'label'] = v_type
|
||||||
|
|
||||||
|
# change the relationship_type to label
|
||||||
|
# so we will see it in dot visualizer
|
||||||
|
for node in data['links']:
|
||||||
|
node[u'label'] = node.pop('relationship_type')
|
@@ -11,18 +11,152 @@
|
|||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
from argparse import ArgumentParser
|
from argparse import ArgumentParser
|
||||||
from argparse import ArgumentTypeError
|
from argparse import ArgumentTypeError
|
||||||
|
|
||||||
|
import json
|
||||||
|
|
||||||
# noinspection PyPackageRequirements
|
# noinspection PyPackageRequirements
|
||||||
import mock
|
import mock
|
||||||
# noinspection PyPackageRequirements
|
# noinspection PyPackageRequirements
|
||||||
|
import six
|
||||||
from testtools import ExpectedException
|
from testtools import ExpectedException
|
||||||
|
|
||||||
|
from vitrageclient.common.formatters import DOTFormatter
|
||||||
from vitrageclient.tests.cli.base import CliTestCase
|
from vitrageclient.tests.cli.base import CliTestCase
|
||||||
from vitrageclient.v1.cli.topology import TopologyShow
|
from vitrageclient.v1.cli.topology import TopologyShow
|
||||||
|
|
||||||
|
JSON_DATA = '''
|
||||||
|
{
|
||||||
|
"directed": true,
|
||||||
|
"graph": {},
|
||||||
|
"links": [
|
||||||
|
{
|
||||||
|
"key": "contains",
|
||||||
|
"source": 0,
|
||||||
|
"vitrage_is_deleted": false,
|
||||||
|
"relationship_type": "contains",
|
||||||
|
"target": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "contains",
|
||||||
|
"source": 1,
|
||||||
|
"vitrage_is_deleted": false,
|
||||||
|
"relationship_type": "contains",
|
||||||
|
"target": 0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"multigraph": true,
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"id": "nova",
|
||||||
|
"vitrage_sample_timestamp": "2018-12-31T13:44:03Z",
|
||||||
|
"vitrage_datasource_name": "nova.zone",
|
||||||
|
"vitrage_operational_state": "OK",
|
||||||
|
"name": "nova",
|
||||||
|
"state": "available",
|
||||||
|
"update_timestamp": "2018-12-31T13:44:03Z",
|
||||||
|
"is_real_vitrage_id": true,
|
||||||
|
"vitrage_id": "05a19de3-e929-4730-ad81-10fa57dcfa0a",
|
||||||
|
"vitrage_aggregated_state": "AVAILABLE",
|
||||||
|
"vitrage_type": "nova.zone",
|
||||||
|
"vitrage_is_deleted": false,
|
||||||
|
"graph_index": 0,
|
||||||
|
"vitrage_category": "RESOURCE",
|
||||||
|
"vitrage_cached_id": "125f1d8c4451a6385cc2cfa2b0ba45be",
|
||||||
|
"vitrage_is_placeholder": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "OpenStack Cluster",
|
||||||
|
"vitrage_sample_timestamp": "2018-12-31T13:44:03Z",
|
||||||
|
"vitrage_operational_state": "OK",
|
||||||
|
"name": "openstack.cluster",
|
||||||
|
"state": "available",
|
||||||
|
"graph_index": 1,
|
||||||
|
"vitrage_id": "070c413e-5a8c-4823-ae20-af44936de2a0",
|
||||||
|
"vitrage_aggregated_state": "AVAILABLE",
|
||||||
|
"vitrage_type": "openstack.cluster",
|
||||||
|
"is_real_vitrage_id": true,
|
||||||
|
"vitrage_category": "RESOURCE",
|
||||||
|
"vitrage_cached_id": "3c7f9d22d9dd1615a00404f86cb3e289",
|
||||||
|
"vitrage_is_deleted": false,
|
||||||
|
"vitrage_is_placeholder": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "ebarilan-devstack",
|
||||||
|
"vitrage_sample_timestamp": "2018-12-31T13:44:03Z",
|
||||||
|
"vitrage_datasource_name": "nova.host",
|
||||||
|
"vitrage_operational_state": "OK",
|
||||||
|
"name": "ebarilan-devstack",
|
||||||
|
"state": "available",
|
||||||
|
"update_timestamp": "2018-12-31T13:44:03Z",
|
||||||
|
"is_real_vitrage_id": true,
|
||||||
|
"vitrage_id": "10da4fa2-397f-4b2e-a43b-937e11ab7daf",
|
||||||
|
"vitrage_aggregated_state": "AVAILABLE",
|
||||||
|
"vitrage_type": "nova.host",
|
||||||
|
"vitrage_is_deleted": false,
|
||||||
|
"graph_index": 2,
|
||||||
|
"vitrage_category": "RESOURCE",
|
||||||
|
"vitrage_cached_id": "9ae4db6fb920e19cb5c57a428b29eb59",
|
||||||
|
"vitrage_is_placeholder": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "b36b4d7a-b309-4b02-9662-5abd79741750",
|
||||||
|
"vitrage_sample_timestamp": "2018-12-31T13:44:04Z",
|
||||||
|
"vitrage_datasource_name": "cinder.volume",
|
||||||
|
"project_id": "210140f1f5a94af99e0adf79a883b75a",
|
||||||
|
"vitrage_operational_state": "OK",
|
||||||
|
"vitrage_aggregated_state": "AVAILABLE",
|
||||||
|
"vitrage_is_placeholder": false,
|
||||||
|
"state": "available",
|
||||||
|
"attachments": [],
|
||||||
|
"graph_index": 3,
|
||||||
|
"vitrage_id": "f0ca9fac-3ebd-4748-97ba-e93a7e7108aa",
|
||||||
|
"size": 1,
|
||||||
|
"vitrage_type": "cinder.volume",
|
||||||
|
"vitrage_is_deleted": false,
|
||||||
|
"vitrage_category": "RESOURCE",
|
||||||
|
"vitrage_cached_id": "f998c5f7bf1851e17e3eea902800a7df",
|
||||||
|
"update_timestamp": "2018-12-31T08:43:32Z",
|
||||||
|
"is_real_vitrage_id": true,
|
||||||
|
"volume_type": "lvmdriver-1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "cebf5d5b-d7b1-4cfb-86fa-f660306b4c1a",
|
||||||
|
"vitrage_sample_timestamp": "2018-12-31T13:44:04Z",
|
||||||
|
"vitrage_datasource_name": "neutron.network",
|
||||||
|
"project_id": "210140f1f5a94af99e0adf79a883b75a",
|
||||||
|
"vitrage_operational_state": "OK",
|
||||||
|
"vitrage_category": "RESOURCE",
|
||||||
|
"vitrage_is_placeholder": false,
|
||||||
|
"state": "ACTIVE",
|
||||||
|
"update_timestamp": "2018-12-30T08:30:33Z",
|
||||||
|
"is_real_vitrage_id": true,
|
||||||
|
"vitrage_id": "eea46e33-81dc-4430-a771-852bac37b43d",
|
||||||
|
"vitrage_aggregated_state": "ACTIVE",
|
||||||
|
"vitrage_type": "neutron.network",
|
||||||
|
"vitrage_is_deleted": false,
|
||||||
|
"graph_index": 4,
|
||||||
|
"name": "public",
|
||||||
|
"vitrage_cached_id": "a0eeca0ab2c865915e23319a2e6d0fd7"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"raw": true
|
||||||
|
}
|
||||||
|
|
||||||
|
'''
|
||||||
|
DOT_DATA = '''\
|
||||||
|
digraph {
|
||||||
|
0 [id=nova, is_real_vitrage_id=True, label="nova\\nnova.zone", state=available, update_timestamp="2018-12-31T13:44:03Z", vitrage_aggregated_state=AVAILABLE, vitrage_cached_id="125f1d8c4451a6385cc2cfa2b0ba45be", vitrage_category=RESOURCE, vitrage_datasource_name="nova.zone", vitrage_id="05a19de3-e929-4730-ad81-10fa57dcfa0a", vitrage_is_deleted=False, vitrage_is_placeholder=False, vitrage_operational_state=OK, vitrage_sample_timestamp="2018-12-31T13:44:03Z", vitrage_type="nova.zone"];
|
||||||
|
1 [id="OpenStack Cluster", is_real_vitrage_id=True, label="openstack.cluster", state=available, vitrage_aggregated_state=AVAILABLE, vitrage_cached_id="3c7f9d22d9dd1615a00404f86cb3e289", vitrage_category=RESOURCE, vitrage_id="070c413e-5a8c-4823-ae20-af44936de2a0", vitrage_is_deleted=False, vitrage_is_placeholder=False, vitrage_operational_state=OK, vitrage_sample_timestamp="2018-12-31T13:44:03Z", vitrage_type="openstack.cluster"];
|
||||||
|
2 [id="ebarilan-devstack", is_real_vitrage_id=True, label="ebarilan-devstack\\nnova.host", state=available, update_timestamp="2018-12-31T13:44:03Z", vitrage_aggregated_state=AVAILABLE, vitrage_cached_id="9ae4db6fb920e19cb5c57a428b29eb59", vitrage_category=RESOURCE, vitrage_datasource_name="nova.host", vitrage_id="10da4fa2-397f-4b2e-a43b-937e11ab7daf", vitrage_is_deleted=False, vitrage_is_placeholder=False, vitrage_operational_state=OK, vitrage_sample_timestamp="2018-12-31T13:44:03Z", vitrage_type="nova.host"];
|
||||||
|
3 [attachments="[]", id="b36b4d7a-b309-4b02-9662-5abd79741750", is_real_vitrage_id=True, label="cinder.volume", project_id="210140f1f5a94af99e0adf79a883b75a", size=1, state=available, update_timestamp="2018-12-31T08:43:32Z", vitrage_aggregated_state=AVAILABLE, vitrage_cached_id=f998c5f7bf1851e17e3eea902800a7df, vitrage_category=RESOURCE, vitrage_datasource_name="cinder.volume", vitrage_id="f0ca9fac-3ebd-4748-97ba-e93a7e7108aa", vitrage_is_deleted=False, vitrage_is_placeholder=False, vitrage_operational_state=OK, vitrage_sample_timestamp="2018-12-31T13:44:04Z", vitrage_type="cinder.volume", volume_type="lvmdriver-1"];
|
||||||
|
4 [id="cebf5d5b-d7b1-4cfb-86fa-f660306b4c1a", is_real_vitrage_id=True, label="public\\nneutron.network", project_id="210140f1f5a94af99e0adf79a883b75a", state=ACTIVE, update_timestamp="2018-12-30T08:30:33Z", vitrage_aggregated_state=ACTIVE, vitrage_cached_id=a0eeca0ab2c865915e23319a2e6d0fd7, vitrage_category=RESOURCE, vitrage_datasource_name="neutron.network", vitrage_id="eea46e33-81dc-4430-a771-852bac37b43d", vitrage_is_deleted=False, vitrage_is_placeholder=False, vitrage_operational_state=OK, vitrage_sample_timestamp="2018-12-31T13:44:04Z", vitrage_type="neutron.network"];
|
||||||
|
0 -> 2 [key=contains, label=contains, vitrage_is_deleted=False];
|
||||||
|
1 -> 0 [key=contains, label=contains, vitrage_is_deleted=False];
|
||||||
|
}
|
||||||
|
''' # noqa
|
||||||
|
|
||||||
|
|
||||||
# noinspection PyAttributeOutsideInit
|
# noinspection PyAttributeOutsideInit
|
||||||
class TopologyShowTest(CliTestCase):
|
class TopologyShowTest(CliTestCase):
|
||||||
@@ -69,3 +203,16 @@ class TopologyShowTest(CliTestCase):
|
|||||||
'--limit', 'spam',
|
'--limit', 'spam',
|
||||||
'--root', 'blabla',
|
'--root', 'blabla',
|
||||||
'--graph-type', 'tree'])
|
'--graph-type', 'tree'])
|
||||||
|
|
||||||
|
def test_dot_emitter(self):
|
||||||
|
def dict2columns(data):
|
||||||
|
return zip(*sorted(data.items()))
|
||||||
|
|
||||||
|
out = six.StringIO()
|
||||||
|
formatter = DOTFormatter()
|
||||||
|
topology = json.loads(JSON_DATA)
|
||||||
|
columns, topology = dict2columns(topology)
|
||||||
|
|
||||||
|
formatter.emit_one(columns, topology, out, None)
|
||||||
|
|
||||||
|
self.assertEqual(DOT_DATA, out.getvalue())
|
||||||
|
@@ -31,6 +31,10 @@ class RcaShow(show.ShowOne):
|
|||||||
|
|
||||||
return parser
|
return parser
|
||||||
|
|
||||||
|
@property
|
||||||
|
def formatter_namespace(self):
|
||||||
|
return 'vitrageclient.formatter.show'
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def formatter_default(self):
|
def formatter_default(self):
|
||||||
return 'json'
|
return 'json'
|
||||||
|
@@ -66,6 +66,10 @@ class TopologyShow(show.ShowOne):
|
|||||||
|
|
||||||
return parser
|
return parser
|
||||||
|
|
||||||
|
@property
|
||||||
|
def formatter_namespace(self):
|
||||||
|
return 'vitrageclient.formatter.show'
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def formatter_default(self):
|
def formatter_default(self):
|
||||||
return 'json'
|
return 'json'
|
||||||
|
Reference in New Issue
Block a user