Skip to content

Commit

Permalink
consider group hierarchy when merging metadata
Browse files Browse the repository at this point in the history
  • Loading branch information
trehn committed Apr 21, 2014
1 parent aeda69c commit 6b890f0
Show file tree
Hide file tree
Showing 3 changed files with 108 additions and 5 deletions.
6 changes: 5 additions & 1 deletion doc/groups.py.rst
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,11 @@ A tuple or list of node names that belong to this group.
``metadata``
------------

A dictionary of arbitrary data that will be accessible from each node's ``node.metadata``. For each node, Blockwart will merge the metadata of all of the node's groups first, then merge in the metadata from the node itself. You should not put conflicting metadata (i.e. dicts that share keys) in groups because there is no control over which group's metadata will be merged in last. You can, of course, put some metadata in a group and then default override it at the node level.
A dictionary of arbitrary data that will be accessible from each node's ``node.metadata``. For each node, Blockwart will merge the metadata of all of the node's groups first, then merge in the metadata from the node itself.

.. warning::

Be careful when defining conflicting metadata (i.e. dictionaries that have some common keys) in multiple groups. Blockwart will consider group hierarchy when merging metadata. For example, it is possible to define a default nameserver for the "eu" group and then override it for the "eu.frankfurt" subgroup. The catch is that this only works for groups that are connected through a subgroup hierarchy. Independent groups will have their metadata merged in an undefined order.

|
Expand Down
51 changes: 48 additions & 3 deletions src/blockwart/node.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
split_items_without_deps
from .exceptions import ItemDependencyError, NodeAlreadyLockedException, RepositoryError
from .items import Item
from .utils import cached_property, LOG, graph_for_items
from .utils import cached_property, LOG, graph_for_items, names
from .utils.text import mark_for_translation as _
from .utils.text import bold, green, red, validate_name, yellow
from .utils.ui import ask_interactively
Expand Down Expand Up @@ -206,6 +206,50 @@ def apply_items(node, workers=1, interactive=False):
)


def _flatten_group_hierarchy(groups):
"""
Takes a list of groups and returns a list of group names ordered so
that parent groups will appear before any of their subgroups.
"""
# dict mapping groups to subgroups
child_groups = {}
for group in groups:
child_groups[group.name] = list(names(group.subgroups))

# dict mapping groups to parent groups
parent_groups = {}
for child_group in child_groups.keys():
parent_groups[child_group] = []
for parent_group, subgroups in child_groups.items():
if child_group in subgroups:
parent_groups[child_group].append(parent_group)

order = []

while True:
top_level_group = None
for group, parents in parent_groups.items():
if parents:
continue
else:
top_level_group = group
break
if not top_level_group:
if parent_groups:
raise RuntimeError(
_("encountered subgroup loop that should have been detected")
)
else:
break
order.append(top_level_group)
del parent_groups[top_level_group]
for group in parent_groups.keys():
if top_level_group in parent_groups[group]:
parent_groups[group].remove(top_level_group)

return order


def format_item_result(result, node, bundle, item, interactive=False):
if result == Item.STATUS_FAILED:
if interactive:
Expand Down Expand Up @@ -376,8 +420,9 @@ def download(self, remote_path, local_path, ignore_failure=False):
@cached_property
def metadata(self):
m = {}
for group in self.groups:
m.update(group.metadata)
group_order = _flatten_group_hierarchy(self.groups)
for group in group_order:
m.update(self.repo.get_group(group).metadata)
m.update(self._node_metadata)
return m

Expand Down
56 changes: 55 additions & 1 deletion tests/unit/node_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from blockwart.exceptions import ItemDependencyError, NodeAlreadyLockedException, RepositoryError
from blockwart.group import Group
from blockwart.items import Item
from blockwart.node import ApplyResult, apply_items, Node, NodeLock
from blockwart.node import ApplyResult, apply_items, _flatten_group_hierarchy, Node, NodeLock
from blockwart.operations import RunResult
from blockwart.repo import Repository
from blockwart.utils import names
Expand Down Expand Up @@ -191,6 +191,60 @@ def test_bs(self):
ApplyResult(MagicMock(), item_results)


class FlattenGroupHierarchyTest(TestCase):
"""
Tests blockwart.node._flatten_group_hierarchy.
"""
def test_reorder_chain(self):
group1 = MagicMock()
group1.name = "group1"
group2 = MagicMock()
group2.name = "group2"
group3 = MagicMock()
group3.name = "group3"

group3.subgroups = [group2]
group2.subgroups = [group1]
group1.subgroups = []

self.assertEqual(
_flatten_group_hierarchy([group1, group2, group3]),
["group3", "group2", "group1"],
)

def test_reorder_short_chain(self):
group1 = MagicMock()
group1.name = "group1"
group2 = MagicMock()
group2.name = "group2"
group3 = MagicMock()
group3.name = "group3"

group3.subgroups = [group2]
group2.subgroups = []
group1.subgroups = []

self.assertEqual(
_flatten_group_hierarchy([group1, group2, group3]),
["group1", "group3", "group2"],
)

def test_loop(self):
group1 = MagicMock()
group1.name = "group1"
group2 = MagicMock()
group2.name = "group2"
group3 = MagicMock()
group3.name = "group3"

group3.subgroups = [group2]
group2.subgroups = [group1]
group1.subgroups = [group3]

with self.assertRaises(RuntimeError):
_flatten_group_hierarchy([group1, group2, group3])


class InitTest(TestCase):
"""
Tests initialization of blockwart.node.Node.
Expand Down

0 comments on commit 6b890f0

Please sign in to comment.