diff --git a/contrib/utils/uml-generator/README.md b/contrib/utils/uml-generator/README.md new file mode 100644 index 00000000..71a3c562 --- /dev/null +++ b/contrib/utils/uml-generator/README.md @@ -0,0 +1,28 @@ +# MuranoPL UML Generator + +This folder contains scripts (currently only one) to generate UML graphs based on MuranoPL manifests. + +## Installation + +1. Copy **umlgen.py** to **meta** folder under MuranoPL directory. + +2. Download **plantuml.jar** from http://plantuml.sourceforge.net/ and copy it to the folder ablve. + +3. Generate UML graph using the command below: + +``` + ./umlgen.py +``` + +## Usage + +``` + ./umlgen.py [--no-namespaces] [--parents-only] [CLASS_FQDN] +``` + +* **--no-namespaces** - disables automatic classes grouping + +* **--parents-only** - generate graph using only parent-child dependencies + +* **CLASS_FQDN** - MuranoPL class FQDN + diff --git a/contrib/utils/uml-generator/umlgen.py b/contrib/utils/uml-generator/umlgen.py new file mode 100755 index 00000000..cc08edb1 --- /dev/null +++ b/contrib/utils/uml-generator/umlgen.py @@ -0,0 +1,225 @@ +#!/usr/bin/python +# Copyright (c) 2013 Mirantis, Inc. +# +# 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 argparse +import os +import re +import yaml + + +# Workaround for unknown yaml tags +def yaml_default_ctor(loader, tag_suffix, node): + return tag_suffix + ' ' + node.value + +yaml.add_multi_constructor('', yaml_default_ctor) + + +class PlantUmlNode(): + def __init__(self, node_dn): + self._dn = node_dn + self.attributes = [] + self.operations = [] + self.extras = [] + + def write(self, f): + f.write('class {0} {{\n'.format(self._dn)) + f.write('-- Properties --\n') + for item in self.attributes: + f.write('<{type}> {name}: {contract}\n'.format(**item)) + f.write('-- Workflows --\n') + for item in self.operations: + f.write('{name}()\n'.format(**item)) + f.write('-- Namespaces --\n') + for item in self.extras: + if item['key'] != '=': + f.write('{key}: {value}\n'.format(**item)) + f.write('}\n') + + def add_attribute(self, name, type, contract): + d = {'type': type, 'name': name, 'contract': contract} + self.attributes.append(d) + + def add_operation(self, name): + d = {'name': name} + self.operations.append(d) + + def add_extra(self, key, value): + d = {'key': key, 'value': value} + self.extras.append(d) + + +class DslSpec(): + def __init__(self, class_dn, basepath='.'): + self._dn = class_dn + self._id = self._dn.replace('.', '_') + self._name = self._dn.split('.')[-1] + self._ns = ('=', self._dn.split('.')[:-1]) + self._graph = None + self.is_virtual = True + self.parent_dn = None + self.manifest = None + + manifest_path = os.path.join(basepath, self._dn, 'manifest.yaml') + if os.path.exists(manifest_path): + self.manifest = yaml.load(open(manifest_path)) + + if self.manifest: + self.is_virtual = False + self.name = self.manifest['Name'] + if 'Extends' in self.manifest: + self.parent_dn = self.get_dn(self.manifest['Extends']) + + def split_ns(self, name=None): + if name: + parts = name.split(':') + if len(parts) == 1: + parts.insert(0, '=') + else: + parts = ['=', self._name] + return parts + + def get_ns(self, name=None): + parts = self.split_ns(name) + return { + 'key': parts[0], + 'value': self.manifest['Namespaces'][parts[0]] + } + + def get_dn(self, name=None): + parts = self.split_ns(name) + parts[0] = self.get_ns(parts[0] + ':')['value'] + return '.'.join(parts) + + def get_name(self): + return self._name + + +class DslPlantUmlNode(DslSpec): + def write(self, file): + uml_class = PlantUmlNode(self._dn) + ext_classes = [] + if not self.is_virtual: + namespaces = [self.get_ns()] + for name, item in self.manifest.get('Properties', {}).iteritems(): + item_type = item.get('Type', 'In') + item_contract = str(item.get('Contract', 'UNDEFINED')) + match = re.search('class\((.*?)\)', item_contract) + if match: + ns = self.get_ns(match.group(1)) + ext_classes.append(self.get_dn(match.group(1))) + if not ns in namespaces: + namespaces.append(ns) + uml_class.add_attribute( + name, + type=item_type, + contract=item_contract + ) + for m in self.manifest.get('Workflow', []): + uml_class.add_operation(m) + for ns in namespaces: + uml_class.add_extra(ns['key'], ns['value']) + uml_class.write(file) + return ext_classes + + +class DslPlantUmlGraph(): + def __init__(self): + self._nodes = [] + self._edges = [] + self._options = {} + self._file = None + + def write(self, classname, level=0): + if level == 0: + self._file.write('@startuml\n') + if self.get_option('NoNamespaces', False): + self._file.write('set namespaceSeparator none\n') + + if self.node_exists(classname): + return + + self.add_node(classname) + + node = DslPlantUmlNode(classname) + ext_classes = node.write(self._file) + + if not self.get_option('ParentsOnly', False): + for ext_class in ext_classes: + self.add_edge( + from_node=ext_class, + to_node=classname, + edge_type='<..' + ) + self.write(ext_class, level + 1) + + if node.is_virtual: + return + + if node.parent_dn: + self.add_edge( + from_node=node.parent_dn, + to_node=classname, + edge_type='<|--' + ) + self.write(node.parent_dn, level + 1) + + if level == 0: + for edge in self._edges: + self._file.write('{from_node} {type} {to_node}\n' + .format(**edge)) + self._file.write('@enduml\n') + self._file.close() + + def add_node(self, classname): + self._nodes.append(classname) + + def node_exists(self, dn): + return dn in self._nodes + + def add_edge(self, from_node, to_node, edge_type): + edge = {'from_node': from_node, 'to_node': to_node, 'type': edge_type} + if not edge in self._edges: + self._edges.append(edge) + + def set_option(self, key, value): + self._options[key] = value + + def get_option(self, key, default=None): + return self._options.get(key, default) + + def open_file(self, file_name): + self._file = open(file_name, 'w') + + def close_file(self): + self._file.close() + + +parser = argparse.ArgumentParser(description='Qwerty') +parser.add_argument('classname', + default='com.mirantis.murano.demoApp.DemoHost', + help='Dsl Class Name to draw.', nargs='?') +parser.add_argument('-n', '--no-namespaces', action='store_true') +parser.add_argument('-p', '--parents-only', action='store_true') +args = parser.parse_args() + +graph = DslPlantUmlGraph() +graph.set_option('NoNamespaces', args.no_namespaces) +graph.set_option('ParentsOnly', args.parents_only) +graph.open_file('plantuml.txt') +graph.write(args.classname) +graph.close_file() + +os.system('java -jar plantuml.jar plantuml.txt') +os.system('xdg-open plantuml.png')