From 2125ae7b143daff4d3fa95de2e318e9224abca57 Mon Sep 17 00:00:00 2001 From: Stephane Thiell Date: Sun, 17 Sep 2023 19:05:38 -0700 Subject: [PATCH] Topology: check that node groups/wildcards are non-empty (#527) Router and destination node sets defined in topology.conf may use NodeSet groups and wildcards but any route definition with an empty node set when we build the tree will be ignored. Add checks for this in the code and add a note to the documentation. Fixes #527. --- doc/sphinx/tools/clush.rst | 13 +- doc/sphinx/tools/nodeset.rst | 2 + lib/ClusterShell/Topology.py | 19 +- tests/TreeTopologyTest.py | 483 ++++++++++++++++++++++------------- 4 files changed, 329 insertions(+), 188 deletions(-) diff --git a/doc/sphinx/tools/clush.rst b/doc/sphinx/tools/clush.rst index e7964809..04535d70 100644 --- a/doc/sphinx/tools/clush.rst +++ b/doc/sphinx/tools/clush.rst @@ -207,14 +207,16 @@ Configuration """"""""""""" The system-wide library configuration file **/etc/clustershell/topology.conf** -defines the routes of default command propagation tree. It is recommended that -all connections between parent and children nodes are carefully +defines available/preferred routes for the command propagation tree. It is +recommended that all connections between parent and children nodes are carefully pre-configured, for example, to avoid any SSH warnings when connecting (if using the default SSH remote worker, of course). .. highlight:: ini -The content of the topology.conf file should look like this:: +The file **topology.conf** is used to define a set of routes under a +``[routes]`` section. Think of it as a routing table but for cluster +commands. Node sets should be used when possible, for example:: [routes] rio0: rio[10-13] @@ -223,7 +225,7 @@ The content of the topology.conf file should look like this:: .. highlight:: text -This file defines the following topology graph:: +The example above defines the following topology graph:: rio0 |- rio[10-11] @@ -231,6 +233,9 @@ This file defines the following topology graph:: `- rio[12-13] `- rio[300-440] +:ref:`nodeset-groups` and :ref:`node-wildcards` are supported in +**topology.conf**, but any route definition with an empty node set +is ignored (a message is printed in debug mode in that case). At runtime, ClusterShell will pick an initial propagation tree from this topology graph definition and the current root node. Multiple admin/root diff --git a/doc/sphinx/tools/nodeset.rst b/doc/sphinx/tools/nodeset.rst index 7e356cbb..57a99b03 100644 --- a/doc/sphinx/tools/nodeset.rst +++ b/doc/sphinx/tools/nodeset.rst @@ -910,6 +910,8 @@ like:: A similar option is available with :ref:`clush-tool`, see :ref:`selecting all nodes with clush `. +.. _node-wildcards: + Node wildcards """""""""""""" diff --git a/lib/ClusterShell/Topology.py b/lib/ClusterShell/Topology.py index 078d6bd5..f14b5ba2 100644 --- a/lib/ClusterShell/Topology.py +++ b/lib/ClusterShell/Topology.py @@ -42,9 +42,14 @@ # Python 2 compat import ConfigParser as configparser +import logging + from ClusterShell.NodeSet import NodeSet +LOGGER = logging.getLogger(__name__) + + class TopologyError(Exception): """topology parser error to report invalid configurations or parsing errors @@ -105,6 +110,7 @@ def add_child(self, child): its parent """ assert isinstance(child, TopologyNodeGroup) + assert len(child.nodeset) > 0, "empty nodeset in child" if child in self._children: return @@ -347,7 +353,6 @@ def add_route(self, src_ns, dst_ns): assert isinstance(src_ns, NodeSet) assert isinstance(dst_ns, NodeSet) - #print 'adding %s -> %s' % (str(src_ns), str(dst_ns)) self._routing.add_route(TopologyRoute(src_ns, dst_ns)) def dest(self, from_nodeset): @@ -456,7 +461,17 @@ def _build_graph(self): """ self.graph = TopologyGraph() for src, dst in self._topology: - self.graph.add_route(NodeSet(src), NodeSet(dst)) + # GH#527: router and destination node sets may use NodeSet groups + # but we ignore any empty sets + src_ns = NodeSet(src) + if not src_ns: + LOGGER.debug('Failed to resolve router node set: %s', src) + dst_ns = NodeSet(dst) + if not dst_ns: + LOGGER.debug('Failed to resolve destination node set "%s" for' \ + 'router node set %s', dst, src_ns) + if src_ns and dst_ns: + self.graph.add_route(src_ns, dst_ns) def tree(self, root, force_rebuild=False): """Return a previously generated propagation tree or build it if diff --git a/tests/TreeTopologyTest.py b/tests/TreeTopologyTest.py index 7f0333c2..727cd827 100644 --- a/tests/TreeTopologyTest.py +++ b/tests/TreeTopologyTest.py @@ -4,7 +4,8 @@ """Unit test for Topology""" import unittest -import tempfile +from tempfile import NamedTemporaryFile +from textwrap import dedent # profiling imports #import cProfile @@ -12,7 +13,10 @@ # --- from ClusterShell.Topology import * -from ClusterShell.NodeSet import NodeSet +from ClusterShell.NodeSet import NodeSet, set_std_group_resolver +from ClusterShell.NodeSet import set_std_group_resolver_config +from ClusterShell.NodeUtils import GroupResolverConfig +from TLib import make_temp_file class TopologyTest(unittest.TestCase): @@ -161,25 +165,25 @@ def testMultipleAdminGroups(self): # TODO : uncommenting following lines should not produce an error. This # is a valid topology!! # ---------- - tmpfile = tempfile.NamedTemporaryFile() - tmpfile.write(b'[routes]\n') - tmpfile.write(b'admin0: nodes[0-1]\n') - #tmpfile.write(b'admin1: nodes[0-1]\n') - tmpfile.write(b'admin2: nodes[2-3]\n') - #tmpfile.write(b'admin3: nodes[2-3]\n') - tmpfile.write(b'nodes[0-1]: nodes[10-19]\n') - tmpfile.write(b'nodes[2-3]: nodes[20-29]\n') - tmpfile.flush() - parser = TopologyParser(tmpfile.name) - - ns_all = NodeSet('admin2,nodes[2-3,20-29]') - ns_tree = NodeSet() - tree = parser.tree('admin2') - self.assertEqual(tree.inner_node_count(), 3) - self.assertEqual(tree.leaf_node_count(), 10) - for nodegroup in tree: - ns_tree.add(nodegroup.nodeset) - self.assertEqual(str(ns_all), str(ns_tree)) + with NamedTemporaryFile() as tmpfile: + tmpfile.write(b'[routes]\n') + tmpfile.write(b'admin0: nodes[0-1]\n') + #tmpfile.write(b'admin1: nodes[0-1]\n') + tmpfile.write(b'admin2: nodes[2-3]\n') + #tmpfile.write(b'admin3: nodes[2-3]\n') + tmpfile.write(b'nodes[0-1]: nodes[10-19]\n') + tmpfile.write(b'nodes[2-3]: nodes[20-29]\n') + tmpfile.flush() + parser = TopologyParser(tmpfile.name) + + ns_all = NodeSet('admin2,nodes[2-3,20-29]') + ns_tree = NodeSet() + tree = parser.tree('admin2') + self.assertEqual(tree.inner_node_count(), 3) + self.assertEqual(tree.leaf_node_count(), 10) + for nodegroup in tree: + ns_tree.add(nodegroup.nodeset) + self.assertEqual(str(ns_all), str(ns_tree)) def testTopologyGraphBigGroups(self): """test adding huge nodegroups in routes""" @@ -196,195 +200,196 @@ def testTopologyGraphBigGroups(self): def testNodeString(self): """test loading a linear string topology""" - tmpfile = tempfile.NamedTemporaryFile() - tmpfile.write(b'[routes]\n') + with NamedTemporaryFile() as tmpfile: + tmpfile.write(b'[routes]\n') - # TODO : increase the size - ns = NodeSet('node[0-10]') + # TODO : increase the size + ns = NodeSet('node[0-10]') - prev = 'admin' - for n in ns: - line = '%s: %s\n' % (prev, str(n)) - tmpfile.write(line.encode()) - prev = n - tmpfile.flush() - parser = TopologyParser(tmpfile.name) + prev = 'admin' + for n in ns: + line = '%s: %s\n' % (prev, str(n)) + tmpfile.write(line.encode()) + prev = n + tmpfile.flush() + parser = TopologyParser(tmpfile.name) - tree = parser.tree('admin') + tree = parser.tree('admin') - ns.add('admin') - ns_tree = NodeSet() - for nodegroup in tree: - ns_tree.add(nodegroup.nodeset) - self.assertEqual(ns, ns_tree) + ns.add('admin') + ns_tree = NodeSet() + for nodegroup in tree: + ns_tree.add(nodegroup.nodeset) + self.assertEqual(ns, ns_tree) def testConfigurationParser(self): """test configuration parsing""" - tmpfile = tempfile.NamedTemporaryFile() - tmpfile.write(b'# this is a comment\n') - tmpfile.write(b'[routes]\n') - tmpfile.write(b'admin: nodes[0-1]\n') - tmpfile.write(b'nodes[0-1]: nodes[2-5]\n') - tmpfile.write(b'nodes[4-5]: nodes[6-9]\n') - tmpfile.flush() - parser = TopologyParser(tmpfile.name) - - parser.tree('admin') - ns_all = NodeSet('admin,nodes[0-9]') - ns_tree = NodeSet() - for nodegroup in parser.tree('admin'): - ns_tree.add(nodegroup.nodeset) - self.assertEqual(str(ns_all), str(ns_tree)) + with NamedTemporaryFile() as tmpfile: + tmpfile.write(b'# this is a comment\n') + tmpfile.write(b'[routes]\n') + tmpfile.write(b'admin: nodes[0-1]\n') + tmpfile.write(b'nodes[0-1]: nodes[2-5]\n') + tmpfile.write(b'nodes[4-5]: nodes[6-9]\n') + tmpfile.flush() + parser = TopologyParser(tmpfile.name) + + parser.tree('admin') + ns_all = NodeSet('admin,nodes[0-9]') + ns_tree = NodeSet() + for nodegroup in parser.tree('admin'): + ns_tree.add(nodegroup.nodeset) + self.assertEqual(str(ns_all), str(ns_tree)) def testConfigurationParserCompatMain(self): """test configuration parsing (Main section compat)""" - tmpfile = tempfile.NamedTemporaryFile() - tmpfile.write(b'# this is a comment\n') - tmpfile.write(b'[Main]\n') - tmpfile.write(b'admin: nodes[0-1]\n') - tmpfile.write(b'nodes[0-1]: nodes[2-5]\n') - tmpfile.write(b'nodes[4-5]: nodes[6-9]\n') - tmpfile.flush() - parser = TopologyParser(tmpfile.name) - - parser.tree('admin') - ns_all = NodeSet('admin,nodes[0-9]') - ns_tree = NodeSet() - for nodegroup in parser.tree('admin'): - ns_tree.add(nodegroup.nodeset) - self.assertEqual(str(ns_all), str(ns_tree)) + with NamedTemporaryFile() as tmpfile: + tmpfile.write(b'# this is a comment\n') + tmpfile.write(b'[Main]\n') + tmpfile.write(b'admin: nodes[0-1]\n') + tmpfile.write(b'nodes[0-1]: nodes[2-5]\n') + tmpfile.write(b'nodes[4-5]: nodes[6-9]\n') + tmpfile.flush() + parser = TopologyParser(tmpfile.name) + + parser.tree('admin') + ns_all = NodeSet('admin,nodes[0-9]') + ns_tree = NodeSet() + for nodegroup in parser.tree('admin'): + ns_tree.add(nodegroup.nodeset) + self.assertEqual(str(ns_all), str(ns_tree)) def testConfigurationShortSyntax(self): """test short topology specification syntax""" - tmpfile = tempfile.NamedTemporaryFile() - tmpfile.write(b'# this is a comment\n') - tmpfile.write(b'[routes]\n') - tmpfile.write(b'admin: nodes[0-9]\n') - tmpfile.write(b'nodes[0-3,5]: nodes[10-19]\n') - tmpfile.write(b'nodes[4,6-9]: nodes[30-39]\n') - tmpfile.flush() - parser = TopologyParser() - parser.load(tmpfile.name) - - ns_all = NodeSet('admin,nodes[0-19,30-39]') - ns_tree = NodeSet() - for nodegroup in parser.tree('admin'): - ns_tree.add(nodegroup.nodeset) - self.assertEqual(str(ns_all), str(ns_tree)) + with NamedTemporaryFile() as tmpfile: + tmpfile.write(b'# this is a comment\n') + tmpfile.write(b'[routes]\n') + tmpfile.write(b'admin: nodes[0-9]\n') + tmpfile.write(b'nodes[0-3,5]: nodes[10-19]\n') + tmpfile.write(b'nodes[4,6-9]: nodes[30-39]\n') + tmpfile.flush() + parser = TopologyParser() + parser.load(tmpfile.name) + + ns_all = NodeSet('admin,nodes[0-19,30-39]') + ns_tree = NodeSet() + for nodegroup in parser.tree('admin'): + ns_tree.add(nodegroup.nodeset) + self.assertEqual(str(ns_all), str(ns_tree)) def testConfigurationLongSyntax(self): """test detailed topology description syntax""" - tmpfile = tempfile.NamedTemporaryFile() - tmpfile.write(b'# this is a comment\n') - tmpfile.write(b'[routes]\n') - tmpfile.write(b'admin: proxy\n') - tmpfile.write(b'proxy: STA[0-1]\n') - tmpfile.write(b'STA0: STB[0-1]\n') - tmpfile.write(b'STB0: nodes[0-2]\n') - tmpfile.write(b'STB1: nodes[3-5]\n') - tmpfile.write(b'STA1: STB[2-3]\n') - tmpfile.write(b'STB2: nodes[6-7]\n') - tmpfile.write(b'STB3: nodes[8-10]\n') - - tmpfile.flush() - parser = TopologyParser() - parser.load(tmpfile.name) - - ns_all = NodeSet('admin,proxy,STA[0-1],STB[0-3],nodes[0-10]') - ns_tree = NodeSet() - tree = parser.tree('admin') - self.assertEqual(tree.inner_node_count(), 8) - self.assertEqual(tree.leaf_node_count(), 11) - for nodegroup in tree: - ns_tree.add(nodegroup.nodeset) - self.assertEqual(str(ns_all), str(ns_tree)) + with NamedTemporaryFile() as tmpfile: + tmpfile.write(b'# this is a comment\n') + tmpfile.write(b'[routes]\n') + tmpfile.write(b'admin: proxy\n') + tmpfile.write(b'proxy: STA[0-1]\n') + tmpfile.write(b'STA0: STB[0-1]\n') + tmpfile.write(b'STB0: nodes[0-2]\n') + tmpfile.write(b'STB1: nodes[3-5]\n') + tmpfile.write(b'STA1: STB[2-3]\n') + tmpfile.write(b'STB2: nodes[6-7]\n') + tmpfile.write(b'STB3: nodes[8-10]\n') + + tmpfile.flush() + parser = TopologyParser() + parser.load(tmpfile.name) + + ns_all = NodeSet('admin,proxy,STA[0-1],STB[0-3],nodes[0-10]') + ns_tree = NodeSet() + tree = parser.tree('admin') + self.assertEqual(tree.inner_node_count(), 8) + self.assertEqual(tree.leaf_node_count(), 11) + for nodegroup in tree: + ns_tree.add(nodegroup.nodeset) + self.assertEqual(str(ns_all), str(ns_tree)) def testConfigurationParserDeepTree(self): """test a configuration that generates a deep tree""" - tmpfile = tempfile.NamedTemporaryFile() - tmpfile.write(b'# this is a comment\n') - tmpfile.write(b'[routes]\n') - tmpfile.write(b'admin: nodes[0-9]\n') - - levels = 15 # how deep do you want the tree to be? - for i in range(0, levels*10, 10): - line = 'nodes[%d-%d]: nodes[%d-%d]\n' % (i, i+9, i+10, i+19) - tmpfile.write(line.encode()) - tmpfile.flush() - parser = TopologyParser() - parser.load(tmpfile.name) - - ns_all = NodeSet('admin,nodes[0-159]') - ns_tree = NodeSet() - tree = parser.tree('admin') - self.assertEqual(tree.inner_node_count(), 151) - self.assertEqual(tree.leaf_node_count(), 10) - for nodegroup in tree: - ns_tree.add(nodegroup.nodeset) - self.assertEqual(str(ns_all), str(ns_tree)) + with NamedTemporaryFile() as tmpfile: + tmpfile.write(b'# this is a comment\n') + tmpfile.write(b'[routes]\n') + tmpfile.write(b'admin: nodes[0-9]\n') + + levels = 15 # how deep do you want the tree to be? + for i in range(0, levels*10, 10): + line = 'nodes[%d-%d]: nodes[%d-%d]\n' % (i, i+9, i+10, i+19) + tmpfile.write(line.encode()) + tmpfile.flush() + parser = TopologyParser() + parser.load(tmpfile.name) + + ns_all = NodeSet('admin,nodes[0-159]') + ns_tree = NodeSet() + tree = parser.tree('admin') + self.assertEqual(tree.inner_node_count(), 151) + self.assertEqual(tree.leaf_node_count(), 10) + for nodegroup in tree: + ns_tree.add(nodegroup.nodeset) + self.assertEqual(str(ns_all), str(ns_tree)) def testConfigurationParserBigTree(self): """test configuration parser against big propagation tree""" - tmpfile = tempfile.NamedTemporaryFile() - tmpfile.write(b'# this is a comment\n') - tmpfile.write(b'[routes]\n') - tmpfile.write(b'admin: ST[0-4]\n') - tmpfile.write(b'ST[0-4]: STA[0-49]\n') - tmpfile.write(b'STA[0-49]: nodes[0-10000]\n') - tmpfile.flush() - parser = TopologyParser() - parser.load(tmpfile.name) - - ns_all = NodeSet('admin,ST[0-4],STA[0-49],nodes[0-10000]') - ns_tree = NodeSet() - tree = parser.tree('admin') - self.assertEqual(tree.inner_node_count(), 56) - self.assertEqual(tree.leaf_node_count(), 10001) - for nodegroup in tree: - ns_tree.add(nodegroup.nodeset) - self.assertEqual(str(ns_all), str(ns_tree)) + with NamedTemporaryFile() as tmpfile: + tmpfile.write(b'# this is a comment\n') + tmpfile.write(b'[routes]\n') + tmpfile.write(b'admin: ST[0-4]\n') + tmpfile.write(b'ST[0-4]: STA[0-49]\n') + tmpfile.write(b'STA[0-49]: nodes[0-10000]\n') + tmpfile.flush() + parser = TopologyParser() + parser.load(tmpfile.name) + + ns_all = NodeSet('admin,ST[0-4],STA[0-49],nodes[0-10000]') + ns_tree = NodeSet() + tree = parser.tree('admin') + self.assertEqual(tree.inner_node_count(), 56) + self.assertEqual(tree.leaf_node_count(), 10001) + for nodegroup in tree: + ns_tree.add(nodegroup.nodeset) + self.assertEqual(str(ns_all), str(ns_tree)) def testConfigurationParserConvergentPaths(self): """convergent paths detection""" - tmpfile = tempfile.NamedTemporaryFile() - tmpfile.write(b'# this is a comment\n') - tmpfile.write(b'[routes]\n') - tmpfile.write(b'fortoy32: fortoy[33-34]\n') - tmpfile.write(b'fortoy33: fortoy35\n') - tmpfile.write(b'fortoy34: fortoy36\n') - tmpfile.write(b'fortoy[35-36]: fortoy37\n') - - tmpfile.flush() - parser = TopologyParser() - self.assertRaises(TopologyError, parser.load, tmpfile.name) + with NamedTemporaryFile() as tmpfile: + tmpfile.write(b'# this is a comment\n') + tmpfile.write(b'[routes]\n') + tmpfile.write(b'fortoy32: fortoy[33-34]\n') + tmpfile.write(b'fortoy33: fortoy35\n') + tmpfile.write(b'fortoy34: fortoy36\n') + tmpfile.write(b'fortoy[35-36]: fortoy37\n') + + tmpfile.flush() + parser = TopologyParser() + self.assertRaises(TopologyError, parser.load, tmpfile.name) def testPrintingTree(self): """test printing tree""" - tmpfile = tempfile.NamedTemporaryFile() - tmpfile.write(b'[routes]\n') - tmpfile.write(b'n0: n[1-2]\n') - tmpfile.write(b'n1: n[10-49]\n') - tmpfile.write(b'n2: n[50-89]\n') - tmpfile.flush() - parser = TopologyParser() - parser.load(tmpfile.name) - - tree = parser.tree('n0') - - # In fact it looks like this: - # --------------------------- - # n0 - # |_ n1 - # | |_ n[10-49] - # |_ n2 - # |_ n[50-89] - # --------------------------- - display_ref1 = 'n0\n|- n1\n| `- n[10-49]\n`- n2\n `- n[50-89]\n' - display_ref2 = 'n0\n|- n2\n| `- n[50-89]\n`- n1\n `- n[10-49]\n' - display = str(tree) - self.assertTrue(display == display_ref1 or display == display_ref2) - - self.assertEqual(str(TopologyTree()), '') + with NamedTemporaryFile() as tmpfile: + tmpfile.write(b'[routes]\n') + tmpfile.write(b'n0: n[1-2]\n') + tmpfile.write(b'n1: n[10-49]\n') + tmpfile.write(b'n2: n[50-89]\n') + tmpfile.flush() + parser = TopologyParser() + parser.load(tmpfile.name) + + tree = parser.tree('n0') + + # In fact it looks like this: + # --------------------------- + # n0 + # |_ n1 + # | |_ n[10-49] + # |_ n2 + # |_ n[50-89] + # --------------------------- + display_ref1 = 'n0\n|- n1\n| `- n[10-49]\n`- n2\n `- n[50-89]\n' + display_ref2 = 'n0\n|- n2\n| `- n[50-89]\n`- n1\n `- n[10-49]\n' + display = str(tree) + self.assertTrue(display == display_ref1 or display == display_ref2) + + self.assertEqual(str(TopologyTree()), + '') def testAddingInvalidChildren(self): """test detecting invalid children""" @@ -436,3 +441,117 @@ def testStrConversions(self): g = TopologyGraph() # XXX: Actually if g is not empty other things will be printed out... self.assertEqual(str(g), '\n') + + +class TopologyWithGroupsTest(unittest.TestCase): + + def setUp(self): + """set default group resolver""" + self.grpf = make_temp_file(dedent(""" + [Main] + default: test + + [test] + map: echo Controller-vm[1,30] + list: echo gw + """).encode()) + set_std_group_resolver_config(self.grpf.name) + + def tearDown(self): + """restore default group resolver""" + set_std_group_resolver(None) # restore std resolver + self.grpf.close() + + def testWildcardsValid(self): + """test topology with node groups and wildcards (valid)""" + # make sure groups are set up + self.assertEqual(str(NodeSet("@gw")), "Controller-vm[1,30]") + + # 1. Test valid NodeSet with wildcards in topology.conf + + with NamedTemporaryFile() as tmpfile: + tmpfile.write(b'[routes]\n') + # (ab)use of valid wildcards + tmpfile.write(b'Controller-?m1:Controller-vm2,?ontroller-vm30\n') + tmpfile.write(b'Controller-vm3?:Computer101\n') + tmpfile.flush() + parser = TopologyParser() + parser.load(tmpfile.name) + + tree = parser.tree('Controller-vm1') + + # Controller-vm1 + # |- Controller-vm2 + # `- Controller-vm30 + # `- Computer101 + + display_ref1 = 'Controller-vm1\n|- Controller-vm2\n' \ + '`- Controller-vm30\n `- Computer101\n' + self.assertEqual(str(tree), display_ref1) + + # 2. Test valid node groups in topology.conf + + with NamedTemporaryFile() as tmpfile: + tmpfile.write(b'[routes]\n') + # use valid wildcards + tmpfile.write(b'@gw:Controller-vm2,Computer101\n') + tmpfile.flush() + parser = TopologyParser() + parser.load(tmpfile.name) + + tree = parser.tree('Controller-vm1') + + # Controller-vm[1,30] + # `- Computer101,Controller-vm2 + + display_ref1 = 'Controller-vm[1,30]\n`' \ + '- Computer101,Controller-vm2\n' + self.assertEqual(str(tree), display_ref1) + + def testWildcardsUnresolvedRouters(self): + """test topology with node groups and wildcards (unresolved routers)""" + with NamedTemporaryFile() as tmpfile: + tmpfile.write(b'[routes]\n') + tmpfile.write(b'Controller-vm1:Controller-vm2,Controller-vm30\n') + tmpfile.write(b'Controller-vm2*:Computer101\n') + tmpfile.flush() + parser = TopologyParser() + parser.load(tmpfile.name) + + tree = parser.tree('Controller-vm1') + + # Controller-vm1 + # `- Controller-vm[2,30] + + display_ref1 = 'Controller-vm1\n`' \ + '- Controller-vm[2,30]\n' + self.assertEqual(str(tree), display_ref1) + + def testWildcardsUnresolvedDestionation(self): + """test topology with node groups and wildcards (unresolved dest)""" + with NamedTemporaryFile() as tmpfile: + tmpfile.write(b'[routes]\n') + tmpfile.write(b'Controller-vm1:Un*resolved3\n') + tmpfile.write(b'Controller-vm30:Computer101\n') + tmpfile.flush() + parser = TopologyParser() + parser.load(tmpfile.name) + + tree = parser.tree('Controller-vm30') + + # Controller-vm30 + # `- Computer101 + + display_ref1 = 'Controller-vm30\n`' \ + '- Computer101\n' + self.assertEqual(str(tree), display_ref1) + + def testWildcardsOverlap(self): + """test topology with node groups and wildcards (overlap)""" + with NamedTemporaryFile() as tmpfile: + tmpfile.write(b'[routes]\n') + tmpfile.write(b'Controller-vm1:@gwController-vm2,Controller-vm30\n') + tmpfile.write(b'Controller-vm30:Computer101\n') + tmpfile.flush() + parser = TopologyParser() + self.assertRaises(TopologyError, parser.load, tmpfile.name)