Blame SOURCES/bz1770975-01-add-resource-relations-command.patch

d01bb5
From 5fa38022bbfe6134f71622174a0961d0e0bc3138 Mon Sep 17 00:00:00 2001
d01bb5
From: Ondrej Mular <omular@redhat.com>
d01bb5
Date: Thu, 14 Nov 2019 13:40:57 +0100
d01bb5
Subject: [PATCH 1/6] add `resource relations` command
d01bb5
d01bb5
This new commad displays relations of a specified resource
d01bb5
---
d01bb5
 pcs/cli/common/lib_wrapper.py                 |   2 +
d01bb5
 pcs/cli/common/printable_tree.py              |  50 ++
d01bb5
 pcs/cli/common/test/test_printable_tree.py    | 216 ++++++
d01bb5
 pcs/cli/resource/relations.py                 | 202 +++++
d01bb5
 pcs/cli/resource/test/test_relations.py       | 492 +++++++++++++
d01bb5
 pcs/common/interface/__init__.py              |   0
d01bb5
 pcs/common/interface/dto.py                   |  18 +
d01bb5
 pcs/common/pacemaker/__init__.py              |   0
d01bb5
 pcs/common/pacemaker/resource/__init__.py     |   0
d01bb5
 pcs/common/pacemaker/resource/relations.py    |  66 ++
d01bb5
 .../pacemaker/resource/test/__init__.py       |   0
d01bb5
 .../pacemaker/resource/test/test_relations.py | 169 +++++
d01bb5
 pcs/lib/cib/resource/__init__.py              |   1 +
d01bb5
 pcs/lib/cib/resource/common.py                |  48 ++
d01bb5
 pcs/lib/cib/resource/relations.py             | 265 +++++++
d01bb5
 pcs/lib/cib/test/test_resource_common.py      | 174 ++++-
d01bb5
 pcs/lib/cib/test/test_resource_relations.py   | 690 ++++++++++++++++++
d01bb5
 pcs/lib/commands/resource.py                  |  22 +
d01bb5
 .../test/resource/test_resource_relations.py  | 300 ++++++++
d01bb5
 pcs/pcs.8                                     |   3 +
d01bb5
 pcs/resource.py                               |   3 +
d01bb5
 pcs/test/tools/fixture_cib.py                 |   3 +
d01bb5
 pcs/usage.py                                  |   7 +
d01bb5
 pcsd/capabilities.xml                         |   8 +
d01bb5
 test/centos7/Dockerfile                       |   4 +-
d01bb5
 25 files changed, 2729 insertions(+), 14 deletions(-)
d01bb5
 create mode 100644 pcs/cli/common/printable_tree.py
d01bb5
 create mode 100644 pcs/cli/common/test/test_printable_tree.py
d01bb5
 create mode 100644 pcs/cli/resource/relations.py
d01bb5
 create mode 100644 pcs/cli/resource/test/test_relations.py
d01bb5
 create mode 100644 pcs/common/interface/__init__.py
d01bb5
 create mode 100644 pcs/common/interface/dto.py
d01bb5
 create mode 100644 pcs/common/pacemaker/__init__.py
d01bb5
 create mode 100644 pcs/common/pacemaker/resource/__init__.py
d01bb5
 create mode 100644 pcs/common/pacemaker/resource/relations.py
d01bb5
 create mode 100644 pcs/common/pacemaker/resource/test/__init__.py
d01bb5
 create mode 100644 pcs/common/pacemaker/resource/test/test_relations.py
d01bb5
 create mode 100644 pcs/lib/cib/resource/relations.py
d01bb5
 create mode 100644 pcs/lib/cib/test/test_resource_relations.py
d01bb5
 create mode 100644 pcs/lib/commands/test/resource/test_resource_relations.py
d01bb5
d01bb5
diff --git a/pcs/cli/common/lib_wrapper.py b/pcs/cli/common/lib_wrapper.py
d01bb5
index bb7ca29d..a6cc0ca8 100644
d01bb5
--- a/pcs/cli/common/lib_wrapper.py
d01bb5
+++ b/pcs/cli/common/lib_wrapper.py
d01bb5
@@ -348,6 +348,8 @@ def load_module(env, middleware_factory, name):
d01bb5
                 "get_failcounts": resource.get_failcounts,
d01bb5
                 "manage": resource.manage,
d01bb5
                 "unmanage": resource.unmanage,
d01bb5
+                "get_resource_relations_tree":
d01bb5
+                    resource.get_resource_relations_tree,
d01bb5
             }
d01bb5
         )
d01bb5
 
d01bb5
diff --git a/pcs/cli/common/printable_tree.py b/pcs/cli/common/printable_tree.py
d01bb5
new file mode 100644
d01bb5
index 00000000..f16efc99
d01bb5
--- /dev/null
d01bb5
+++ b/pcs/cli/common/printable_tree.py
d01bb5
@@ -0,0 +1,50 @@
d01bb5
+class PrintableTreeNode(object):
d01bb5
+    @property
d01bb5
+    def members(self):
d01bb5
+        raise NotImplementedError()
d01bb5
+
d01bb5
+    @property
d01bb5
+    def detail(self):
d01bb5
+        raise NotImplementedError()
d01bb5
+
d01bb5
+    @property
d01bb5
+    def is_leaf(self):
d01bb5
+        raise NotImplementedError()
d01bb5
+
d01bb5
+    def get_title(self, verbose):
d01bb5
+        raise NotImplementedError()
d01bb5
+
d01bb5
+
d01bb5
+def tree_to_lines(node, verbose=False, title_prefix="", indent=""):
d01bb5
+    """
d01bb5
+    Return sequence of strings representing lines to print out tree structure on
d01bb5
+    command line.
d01bb5
+    """
d01bb5
+    result = []
d01bb5
+    note = ""
d01bb5
+    if node.is_leaf:
d01bb5
+        note = " [displayed elsewhere]"
d01bb5
+    title = node.get_title(verbose)
d01bb5
+    result.append("{}{}{}".format(title_prefix, title, note))
d01bb5
+    if node.is_leaf:
d01bb5
+        return result
d01bb5
+    _indent = "|  "
d01bb5
+    if not node.members:
d01bb5
+        _indent = "   "
d01bb5
+    for line in node.detail:
d01bb5
+        result.append("{}{}{}".format(indent, _indent, line))
d01bb5
+    _indent = "|  "
d01bb5
+    _title_prefix = "|- "
d01bb5
+    for member in node.members:
d01bb5
+        if member == node.members[-1]:
d01bb5
+            _indent = "   "
d01bb5
+            _title_prefix = "`- "
d01bb5
+        result.extend(
d01bb5
+            tree_to_lines(
d01bb5
+                member,
d01bb5
+                verbose,
d01bb5
+                indent="{}{}".format(indent, _indent),
d01bb5
+                title_prefix="{}{}".format(indent, _title_prefix),
d01bb5
+            )
d01bb5
+        )
d01bb5
+    return result
d01bb5
diff --git a/pcs/cli/common/test/test_printable_tree.py b/pcs/cli/common/test/test_printable_tree.py
d01bb5
new file mode 100644
d01bb5
index 00000000..6f4757ee
d01bb5
--- /dev/null
d01bb5
+++ b/pcs/cli/common/test/test_printable_tree.py
d01bb5
@@ -0,0 +1,216 @@
d01bb5
+from pcs.test.tools.pcs_unittest import TestCase
d01bb5
+# from unittest import TestCase
d01bb5
+
d01bb5
+from pcs.cli.common import printable_tree as lib
d01bb5
+
d01bb5
+
d01bb5
+class Node(object):
d01bb5
+    def __init__(self, title, detail, is_leaf, members):
d01bb5
+        self.title = title
d01bb5
+        self.detail = detail
d01bb5
+        self.is_leaf = is_leaf
d01bb5
+        self.members = members
d01bb5
+
d01bb5
+    def get_title(self, verbose):
d01bb5
+        detail = " (verbose)" if verbose else ""
d01bb5
+        return "{}{}".format(self.title, detail)
d01bb5
+
d01bb5
+def node(an_id, detail=0, leaf=False, members=None):
d01bb5
+    return Node(
d01bb5
+        "{}-title".format(an_id),
d01bb5
+        ["{}-detail{}".format(an_id, i) for i in range(detail)],
d01bb5
+        leaf,
d01bb5
+        members=members or [],
d01bb5
+    )
d01bb5
+
d01bb5
+class TreeToLines(TestCase):
d01bb5
+    def test_verbose(self):
d01bb5
+        self.assertEqual(
d01bb5
+            ["l0-title (verbose)"], lib.tree_to_lines(node("l0"), verbose=True)
d01bb5
+        )
d01bb5
+
d01bb5
+    def test_empty(self):
d01bb5
+        self.assertEqual(["l0-title"], lib.tree_to_lines(node("l0")))
d01bb5
+
d01bb5
+    def test_empty_leaf(self):
d01bb5
+        self.assertEqual(
d01bb5
+            ["l0-title [displayed elsewhere]"],
d01bb5
+            lib.tree_to_lines(node("l0", leaf=True)),
d01bb5
+        )
d01bb5
+
d01bb5
+    def test_detail_simple(self):
d01bb5
+        self.assertEqual(
d01bb5
+            [
d01bb5
+                "l0-title",
d01bb5
+                "   l0-detail0",
d01bb5
+            ],
d01bb5
+            lib.tree_to_lines(node("l0", 1)),
d01bb5
+        )
d01bb5
+
d01bb5
+    def test_detail(self):
d01bb5
+        self.assertEqual(
d01bb5
+            [
d01bb5
+                "l0-title",
d01bb5
+                "   l0-detail0",
d01bb5
+                "   l0-detail1",
d01bb5
+                "   l0-detail2",
d01bb5
+            ],
d01bb5
+            lib.tree_to_lines(node("l0", 3)),
d01bb5
+        )
d01bb5
+
d01bb5
+    def test_detail_leaf(self):
d01bb5
+        self.assertEqual(
d01bb5
+            ["l0-title [displayed elsewhere]"],
d01bb5
+            lib.tree_to_lines(node("l0", 3, True)),
d01bb5
+        )
d01bb5
+
d01bb5
+    def test_one_member(self):
d01bb5
+        self.assertEqual(
d01bb5
+            [
d01bb5
+                "l0-title",
d01bb5
+                "`- l1-title",
d01bb5
+            ],
d01bb5
+            lib.tree_to_lines(node("l0", members=[node("l1")])),
d01bb5
+        )
d01bb5
+
d01bb5
+    def test_one_member_leaf(self):
d01bb5
+        self.assertEqual(
d01bb5
+            ["l0-title [displayed elsewhere]"],
d01bb5
+            lib.tree_to_lines(node("l0", leaf=True, members=[node("l1")])),
d01bb5
+        )
d01bb5
+
d01bb5
+    def test_multiple_members(self):
d01bb5
+        self.assertEqual(
d01bb5
+            [
d01bb5
+                "l0-title",
d01bb5
+                "|- l1-title",
d01bb5
+                "|- l2-title",
d01bb5
+                "`- l3-title",
d01bb5
+            ],
d01bb5
+            lib.tree_to_lines(
d01bb5
+                node("l0", members=[node("l1"), node("l2"), node("l3")])
d01bb5
+            ),
d01bb5
+        )
d01bb5
+
d01bb5
+    def test_multiple_members_detail(self):
d01bb5
+        self.assertEqual(
d01bb5
+            [
d01bb5
+                "l0-title",
d01bb5
+                "|  l0-detail0",
d01bb5
+                "|  l0-detail1",
d01bb5
+                "|- l1-title",
d01bb5
+                "|- l2-title",
d01bb5
+                "`- l3-title",
d01bb5
+            ],
d01bb5
+            lib.tree_to_lines(
d01bb5
+                node(
d01bb5
+                    "l0", detail=2, members=[node("l1"), node("l2"), node("l3")]
d01bb5
+                )
d01bb5
+            ),
d01bb5
+        )
d01bb5
+
d01bb5
+    def test_multiple_members_detail_leaf(self):
d01bb5
+        self.assertEqual(
d01bb5
+            ["l0-title [displayed elsewhere]"],
d01bb5
+            lib.tree_to_lines(
d01bb5
+                node("l0", 2, True, [node("l1"), node("l2"), node("l3")])
d01bb5
+            ),
d01bb5
+        )
d01bb5
+
d01bb5
+    def test_complex_tree_wide(self):
d01bb5
+        self.assertEqual(
d01bb5
+            [
d01bb5
+                "0-title",
d01bb5
+                "|  0-detail0",
d01bb5
+                "|  0-detail1",
d01bb5
+                "|- 00-title",
d01bb5
+                "|     00-detail0",
d01bb5
+                "|     00-detail1",
d01bb5
+                "|- 01-title",
d01bb5
+                "|  |- 010-title",
d01bb5
+                "|  |  `- 0100-title [displayed elsewhere]",
d01bb5
+                "|  `- 011-title",
d01bb5
+                "|- 02-title",
d01bb5
+                "|  |  02-detail0",
d01bb5
+                "|  |  02-detail1",
d01bb5
+                "|  |  02-detail2",
d01bb5
+                "|  |- 020-title",
d01bb5
+                "|  `- 021-title",
d01bb5
+                "|     |  021-detail0",
d01bb5
+                "|     |- 0210-title [displayed elsewhere]",
d01bb5
+                "|     |- 0211-title",
d01bb5
+                "|     |     0211-detail0",
d01bb5
+                "|     |     0211-detail1",
d01bb5
+                "|     `- 0212-title",
d01bb5
+                "|        `- 02120-title",
d01bb5
+                "|- 03-title",
d01bb5
+                "|  `- 030-title",
d01bb5
+                "|     `- 0300-title",
d01bb5
+                "|- 04-title [displayed elsewhere]",
d01bb5
+                "`- 05-title",
d01bb5
+                "      05-detail0",
d01bb5
+            ],
d01bb5
+            lib.tree_to_lines(
d01bb5
+                node(
d01bb5
+                    "0",
d01bb5
+                    2,
d01bb5
+                    members=[
d01bb5
+                        node("00", 2),
d01bb5
+                        node(
d01bb5
+                            "01", members=[
d01bb5
+                                node("010", members=[node("0100", leaf=True)]),
d01bb5
+                                node("011"),
d01bb5
+                            ],
d01bb5
+                        ),
d01bb5
+                        node(
d01bb5
+                            "02", 3, members=[
d01bb5
+                                node("020"),
d01bb5
+                                node(
d01bb5
+                                    "021", 1, members=[
d01bb5
+                                        node("0210", leaf=True),
d01bb5
+                                        node("0211", 2),
d01bb5
+                                        node("0212", members=[node("02120")]),
d01bb5
+                                    ],
d01bb5
+                                ),
d01bb5
+                            ],
d01bb5
+                        ),
d01bb5
+                        node(
d01bb5
+                            "03", members=[node("030", members=[node("0300")])],
d01bb5
+                        ),
d01bb5
+                        node("04", leaf=True),
d01bb5
+                        node("05", 1),
d01bb5
+                    ],
d01bb5
+                )
d01bb5
+            ),
d01bb5
+        )
d01bb5
+
d01bb5
+    def test_complex_tree_deep(self):
d01bb5
+        self.assertEqual(
d01bb5
+            [
d01bb5
+                "0-title",
d01bb5
+                "|  0-detail0",
d01bb5
+                "`- 00-title",
d01bb5
+                "   |- 000-title",
d01bb5
+                "   |     000-detail0",
d01bb5
+                "   |     000-detail1",
d01bb5
+                "   `- 001-title",
d01bb5
+                "      |- 0010-title [displayed elsewhere]",
d01bb5
+                "      |- 0011-title",
d01bb5
+                "      |     0011-detail0",
d01bb5
+                "      |     0011-detail1",
d01bb5
+                "      `- 0012-title",
d01bb5
+                "         `- 00120-title",
d01bb5
+            ],
d01bb5
+            lib.tree_to_lines(
d01bb5
+                node("0", 1, members=[
d01bb5
+                    node("00", members=[
d01bb5
+                        node("000", 2),
d01bb5
+                        node("001", members=[
d01bb5
+                            node("0010", leaf=True),
d01bb5
+                            node("0011", 2),
d01bb5
+                            node("0012", members=[node("00120")]),
d01bb5
+                        ]),
d01bb5
+                    ]),
d01bb5
+                ])
d01bb5
+            ),
d01bb5
+        )
d01bb5
diff --git a/pcs/cli/resource/relations.py b/pcs/cli/resource/relations.py
d01bb5
new file mode 100644
d01bb5
index 00000000..07d34d4d
d01bb5
--- /dev/null
d01bb5
+++ b/pcs/cli/resource/relations.py
d01bb5
@@ -0,0 +1,202 @@
d01bb5
+from __future__ import print_function
d01bb5
+
d01bb5
+from pcs.common.pacemaker.resource.relations import (
d01bb5
+    ResourceRelationDto,
d01bb5
+    ResourceRelationType,
d01bb5
+)
d01bb5
+from pcs.cli.common.console_report import format_optional
d01bb5
+from pcs.cli.common.errors import CmdLineInputError
d01bb5
+from pcs.cli.common.printable_tree import (
d01bb5
+    tree_to_lines,
d01bb5
+    PrintableTreeNode,
d01bb5
+)
d01bb5
+
d01bb5
+
d01bb5
+def show_resource_relations_cmd(lib, argv, modifiers):
d01bb5
+    """
d01bb5
+    Options:
d01bb5
+      * -f - CIB file
d01bb5
+      * --full - show constraint ids and resource types
d01bb5
+    """
d01bb5
+    if len(argv) != 1:
d01bb5
+        raise CmdLineInputError()
d01bb5
+    tree = ResourcePrintableNode.from_dto(
d01bb5
+        ResourceRelationDto.from_dict(
d01bb5
+            lib.resource.get_resource_relations_tree(argv[0])
d01bb5
+        )
d01bb5
+    )
d01bb5
+    for line in tree_to_lines(tree, verbose=modifiers["full"]):
d01bb5
+        print(line)
d01bb5
+
d01bb5
+
d01bb5
+class ResourceRelationBase(PrintableTreeNode):
d01bb5
+    def __init__( self, relation_entity, members, is_leaf):
d01bb5
+        self._relation_entity = relation_entity
d01bb5
+        self._members = members
d01bb5
+        self._is_leaf = is_leaf
d01bb5
+
d01bb5
+    @property
d01bb5
+    def is_leaf(self):
d01bb5
+        return self._is_leaf
d01bb5
+
d01bb5
+    @property
d01bb5
+    def relation_entity(self):
d01bb5
+        return self._relation_entity
d01bb5
+
d01bb5
+    @property
d01bb5
+    def members(self):
d01bb5
+        return self._members
d01bb5
+
d01bb5
+    @property
d01bb5
+    def detail(self):
d01bb5
+        raise NotImplementedError()
d01bb5
+
d01bb5
+    def get_title(self, verbose):
d01bb5
+        raise NotImplementedError()
d01bb5
+
d01bb5
+
d01bb5
+class ResourcePrintableNode(ResourceRelationBase):
d01bb5
+    @classmethod
d01bb5
+    def from_dto(cls, resource_dto):
d01bb5
+        def _relation_comparator(item):
d01bb5
+            type_priorities = (
d01bb5
+                ResourceRelationType.INNER_RESOURCES,
d01bb5
+                ResourceRelationType.OUTER_RESOURCE,
d01bb5
+                ResourceRelationType.ORDER,
d01bb5
+                ResourceRelationType.ORDER_SET,
d01bb5
+            )
d01bb5
+            priority_map = {
d01bb5
+                _type: value for value, _type in enumerate(type_priorities)
d01bb5
+            }
d01bb5
+            return "{_type}_{_id}".format(
d01bb5
+                _type=priority_map.get(
d01bb5
+                    # Hardcoded number 9 is intentional. If there is more than
d01bb5
+                    # 10 items, it would be required to also prepend zeros for
d01bb5
+                    # lower numbers. E.g: if there is 100 options, it should
d01bb5
+                    # starts as 000, 001, ...
d01bb5
+                    item.relation_entity.type, 9 # type: ignore
d01bb5
+                ),
d01bb5
+                _id=item.relation_entity.id
d01bb5
+            )
d01bb5
+
d01bb5
+        return cls(
d01bb5
+            resource_dto.relation_entity,
d01bb5
+            sorted(
d01bb5
+                [
d01bb5
+                    RelationPrintableNode.from_dto(member_dto)
d01bb5
+                    for member_dto in resource_dto.members
d01bb5
+                ],
d01bb5
+                key=_relation_comparator,
d01bb5
+            ),
d01bb5
+            resource_dto.is_leaf
d01bb5
+        )
d01bb5
+
d01bb5
+    def get_title(self, verbose):
d01bb5
+        rsc_type = self._relation_entity.type
d01bb5
+        metadata = self._relation_entity.metadata
d01bb5
+        if rsc_type == "primitive":
d01bb5
+            rsc_type = "{_class}{_provider}{_type}".format(
d01bb5
+                _class=format_optional(metadata.get("class"), "{}:"),
d01bb5
+                _provider=format_optional(metadata.get("provider"), "{}:"),
d01bb5
+                _type=metadata.get("type"),
d01bb5
+            )
d01bb5
+        detail = " (resource: {})".format(rsc_type) if verbose else ""
d01bb5
+        return "{}{}".format(self._relation_entity.id, detail)
d01bb5
+
d01bb5
+    @property
d01bb5
+    def detail(self):
d01bb5
+        return []
d01bb5
+
d01bb5
+
d01bb5
+class RelationPrintableNode(ResourceRelationBase):
d01bb5
+    @classmethod
d01bb5
+    def from_dto(cls, relation_dto):
d01bb5
+        return cls(
d01bb5
+            relation_dto.relation_entity,
d01bb5
+            sorted(
d01bb5
+                [
d01bb5
+                    ResourcePrintableNode.from_dto(member_dto)
d01bb5
+                    for member_dto in relation_dto.members
d01bb5
+                ],
d01bb5
+                key=lambda item: item.relation_entity.id,
d01bb5
+            ),
d01bb5
+            relation_dto.is_leaf
d01bb5
+        )
d01bb5
+
d01bb5
+    def get_title(self, verbose):
d01bb5
+        rel_type_map = {
d01bb5
+            ResourceRelationType.ORDER: "order",
d01bb5
+            ResourceRelationType.ORDER_SET: "order set",
d01bb5
+            ResourceRelationType.INNER_RESOURCES: "inner resource(s)",
d01bb5
+            ResourceRelationType.OUTER_RESOURCE: "outer resource",
d01bb5
+        }
d01bb5
+        detail = (
d01bb5
+            " ({})".format(self._relation_entity.metadata.get("id"))
d01bb5
+            if verbose
d01bb5
+            else ""
d01bb5
+        )
d01bb5
+        return "{type}{detail}".format(
d01bb5
+            type=rel_type_map.get(self._relation_entity.type, "<unknown>"),
d01bb5
+            detail=detail,
d01bb5
+        )
d01bb5
+
d01bb5
+    @property
d01bb5
+    def detail(self):
d01bb5
+        ent = self._relation_entity
d01bb5
+        if ent.type == ResourceRelationType.ORDER:
d01bb5
+            return _order_metadata_to_str(ent.metadata)
d01bb5
+        if ent.type == ResourceRelationType.ORDER_SET:
d01bb5
+            return _order_set_metadata_to_str(ent.metadata)
d01bb5
+        if (
d01bb5
+            ent.type == ResourceRelationType.INNER_RESOURCES
d01bb5
+            and
d01bb5
+            len(ent.members) > 1
d01bb5
+        ):
d01bb5
+            return ["members: {}".format(" ".join(ent.members))]
d01bb5
+        return []
d01bb5
+
d01bb5
+
d01bb5
+def _order_metadata_to_str(metadata):
d01bb5
+    return [
d01bb5
+        "{action1} {resource1} then {action2} {resource2}".format(
d01bb5
+            action1=metadata["first-action"],
d01bb5
+            resource1=metadata["first"],
d01bb5
+            action2=metadata["then-action"],
d01bb5
+            resource2=metadata["then"],
d01bb5
+        )
d01bb5
+    ] + _order_common_metadata_to_str(metadata)
d01bb5
+
d01bb5
+
d01bb5
+def _order_set_metadata_to_str(metadata):
d01bb5
+    result = []
d01bb5
+    for res_set in metadata["sets"]:
d01bb5
+        result.append("   set {resources}{options}".format(
d01bb5
+            resources=" ".join(res_set["members"]),
d01bb5
+            options=_resource_set_options_to_str(res_set["metadata"]),
d01bb5
+        ))
d01bb5
+    return _order_common_metadata_to_str(metadata) + result
d01bb5
+
d01bb5
+
d01bb5
+def _resource_set_options_to_str(metadata):
d01bb5
+    supported_keys = (
d01bb5
+        "sequential", "require-all", "ordering", "action", "role", "kind",
d01bb5
+        "score",
d01bb5
+    )
d01bb5
+    result = _filter_supported_keys(metadata, supported_keys)
d01bb5
+    return " ({})".format(result) if result else ""
d01bb5
+
d01bb5
+
d01bb5
+def _filter_supported_keys(data, supported_keys):
d01bb5
+    return " ".join([
d01bb5
+        "{}={}".format(key, value)
d01bb5
+        for key, value in sorted(data.items())
d01bb5
+        if key in supported_keys
d01bb5
+    ])
d01bb5
+
d01bb5
+
d01bb5
+def _order_common_metadata_to_str(metadata):
d01bb5
+    result = _filter_supported_keys(
d01bb5
+        metadata, ("symmetrical", "kind", "require-all", "score")
d01bb5
+    )
d01bb5
+    return [result] if result else []
d01bb5
+
d01bb5
diff --git a/pcs/cli/resource/test/test_relations.py b/pcs/cli/resource/test/test_relations.py
d01bb5
new file mode 100644
d01bb5
index 00000000..77c1c782
d01bb5
--- /dev/null
d01bb5
+++ b/pcs/cli/resource/test/test_relations.py
d01bb5
@@ -0,0 +1,492 @@
d01bb5
+from pcs.test.tools.pcs_unittest import mock, TestCase
d01bb5
+
d01bb5
+from pcs.common.pacemaker.resource.relations import (
d01bb5
+    RelationEntityDto,
d01bb5
+    ResourceRelationDto,
d01bb5
+    ResourceRelationType,
d01bb5
+)
d01bb5
+from pcs.cli.common.errors import CmdLineInputError
d01bb5
+from pcs.cli.resource import relations
d01bb5
+
d01bb5
+
d01bb5
+DEFAULT_MODIFIERS = {"full": False}
d01bb5
+
d01bb5
+
d01bb5
+class ShowResourceRelationsCmd(TestCase):
d01bb5
+    def setUp(self):
d01bb5
+        self.maxDiff = None
d01bb5
+        self.lib_call = mock.Mock()
d01bb5
+        self.lib = mock.Mock(spec_set=["resource"])
d01bb5
+        self.lib.resource = mock.Mock(spec_set=["get_resource_relations_tree"])
d01bb5
+        self.lib.resource.get_resource_relations_tree = self.lib_call
d01bb5
+        self.lib_call.return_value = ResourceRelationDto(
d01bb5
+            RelationEntityDto(
d01bb5
+                "d1", "primitive", [], {
d01bb5
+                    "class": "ocf",
d01bb5
+                    "provider": "pacemaker",
d01bb5
+                    "type": "Dummy",
d01bb5
+                }
d01bb5
+            ),
d01bb5
+            [
d01bb5
+                ResourceRelationDto(
d01bb5
+                    RelationEntityDto(
d01bb5
+                        "order1", ResourceRelationType.ORDER, [], {
d01bb5
+                            "first-action": "start",
d01bb5
+                            "first": "d1",
d01bb5
+                            "then-action": "start",
d01bb5
+                            "then": "d2",
d01bb5
+                            "kind": "Mandatory",
d01bb5
+                            "symmetrical": "true",
d01bb5
+                        }
d01bb5
+                    ),
d01bb5
+                    [
d01bb5
+                        ResourceRelationDto(
d01bb5
+                            RelationEntityDto(
d01bb5
+                                "d2", "primitive", [], {
d01bb5
+                                    "class": "ocf",
d01bb5
+                                    "provider": "heartbeat",
d01bb5
+                                    "type": "Dummy",
d01bb5
+                                }
d01bb5
+                            ),
d01bb5
+                            [],
d01bb5
+                            False
d01bb5
+                        ),
d01bb5
+                    ],
d01bb5
+                    False
d01bb5
+                ),
d01bb5
+                ResourceRelationDto(
d01bb5
+                    RelationEntityDto(
d01bb5
+                        "inner:g1", ResourceRelationType.INNER_RESOURCES, [], {}
d01bb5
+                    ),
d01bb5
+                    [
d01bb5
+                        ResourceRelationDto(
d01bb5
+                            RelationEntityDto("g1", "group", [], {}),
d01bb5
+                            [],
d01bb5
+                            True,
d01bb5
+                        ),
d01bb5
+                    ],
d01bb5
+                    False
d01bb5
+                )
d01bb5
+            ],
d01bb5
+            False,
d01bb5
+        ).to_dict()
d01bb5
+
d01bb5
+    def test_no_args(self):
d01bb5
+        with self.assertRaises(CmdLineInputError) as cm:
d01bb5
+            relations.show_resource_relations_cmd(
d01bb5
+                self.lib, [], DEFAULT_MODIFIERS
d01bb5
+            )
d01bb5
+        self.assertIsNone(cm.exception.message)
d01bb5
+
d01bb5
+    def test_more_args(self):
d01bb5
+        with self.assertRaises(CmdLineInputError) as cm:
d01bb5
+            relations.show_resource_relations_cmd(
d01bb5
+                self.lib, ["a1", "a2"], DEFAULT_MODIFIERS
d01bb5
+            )
d01bb5
+        self.assertIsNone(cm.exception.message)
d01bb5
+
d01bb5
+    @mock.patch("pcs.cli.resource.relations.print")
d01bb5
+    def test_success(self, mock_print):
d01bb5
+        relations.show_resource_relations_cmd(
d01bb5
+            self.lib, ["d1"], DEFAULT_MODIFIERS
d01bb5
+        )
d01bb5
+        self.lib_call.assert_called_once_with("d1")
d01bb5
+        self.assertEqual(
d01bb5
+            [
d01bb5
+                mock.call("d1"),
d01bb5
+                mock.call("|- inner resource(s)"),
d01bb5
+                mock.call("|  `- g1 [displayed elsewhere]"),
d01bb5
+                mock.call("`- order"),
d01bb5
+                mock.call("   |  start d1 then start d2"),
d01bb5
+                mock.call("   |  kind=Mandatory symmetrical=true"),
d01bb5
+                mock.call("   `- d2"),
d01bb5
+            ],
d01bb5
+            mock_print.call_args_list
d01bb5
+        )
d01bb5
+
d01bb5
+    @mock.patch("pcs.cli.resource.relations.print")
d01bb5
+    def test_verbose(self, mock_print):
d01bb5
+        relations.show_resource_relations_cmd(self.lib, ["d1"], {"full": True})
d01bb5
+        self.lib_call.assert_called_once_with("d1")
d01bb5
+        self.assertEqual(
d01bb5
+            [
d01bb5
+                mock.call("d1 (resource: ocf:pacemaker:Dummy)"),
d01bb5
+                mock.call("|- inner resource(s) (None)"),
d01bb5
+                mock.call("|  `- g1 (resource: group) [displayed elsewhere]"),
d01bb5
+                mock.call("`- order (None)"),
d01bb5
+                mock.call("   |  start d1 then start d2"),
d01bb5
+                mock.call("   |  kind=Mandatory symmetrical=true"),
d01bb5
+                mock.call("   `- d2 (resource: ocf:heartbeat:Dummy)"),
d01bb5
+            ],
d01bb5
+            mock_print.call_args_list
d01bb5
+        )
d01bb5
+
d01bb5
+
d01bb5
+def _fixture_dummy(_id):
d01bb5
+    return RelationEntityDto(
d01bb5
+    _id, "primitive", [], {
d01bb5
+        "class": "ocf",
d01bb5
+        "provider": "pacemaker",
d01bb5
+        "type": "Dummy",
d01bb5
+    }
d01bb5
+)
d01bb5
+
d01bb5
+
d01bb5
+D1_PRIMITIVE = _fixture_dummy("d1")
d01bb5
+D2_PRIMITIVE = _fixture_dummy("d2")
d01bb5
+
d01bb5
+
d01bb5
+def _fixture_res_rel_dto(ent):
d01bb5
+    return ResourceRelationDto(ent, [], True)
d01bb5
+
d01bb5
+
d01bb5
+class ResourcePrintableNode(TestCase):
d01bb5
+    def assert_member(self, member, ent):
d01bb5
+        self.assertTrue(isinstance(member, relations.RelationPrintableNode))
d01bb5
+        self.assertEqual(ent, member.relation_entity)
d01bb5
+        self.assertEqual(True, member.is_leaf)
d01bb5
+        self.assertEqual(0, len(member.members))
d01bb5
+
d01bb5
+    def test_from_dto(self):
d01bb5
+        inner_ent = RelationEntityDto(
d01bb5
+            "inner:g1", ResourceRelationType.INNER_RESOURCES, [], {}
d01bb5
+        )
d01bb5
+        outer_ent = RelationEntityDto(
d01bb5
+            "outer:g1", ResourceRelationType.OUTER_RESOURCE, [], {}
d01bb5
+        )
d01bb5
+        order_ent1 = RelationEntityDto(
d01bb5
+            "order1", ResourceRelationType.ORDER, [], {}
d01bb5
+        )
d01bb5
+        order_ent2 = RelationEntityDto(
d01bb5
+            "order2", ResourceRelationType.ORDER, [], {}
d01bb5
+        )
d01bb5
+        order_set_ent = RelationEntityDto(
d01bb5
+            "order_set", ResourceRelationType.ORDER_SET, [], {}
d01bb5
+        )
d01bb5
+
d01bb5
+        dto = ResourceRelationDto(
d01bb5
+            D1_PRIMITIVE,
d01bb5
+            [
d01bb5
+                _fixture_res_rel_dto(order_set_ent),
d01bb5
+                _fixture_res_rel_dto(order_ent2),
d01bb5
+                _fixture_res_rel_dto(outer_ent),
d01bb5
+                _fixture_res_rel_dto(inner_ent),
d01bb5
+                _fixture_res_rel_dto(order_ent1),
d01bb5
+            ],
d01bb5
+            False,
d01bb5
+        )
d01bb5
+        obj = relations.ResourcePrintableNode.from_dto(dto)
d01bb5
+        self.assertEqual(D1_PRIMITIVE, obj.relation_entity)
d01bb5
+        self.assertEqual(False, obj.is_leaf)
d01bb5
+        expected_members = (
d01bb5
+            inner_ent, outer_ent, order_ent1, order_ent2, order_set_ent
d01bb5
+        )
d01bb5
+        self.assertEqual(len(expected_members), len(obj.members))
d01bb5
+        for i, member in enumerate(obj.members):
d01bb5
+            self.assert_member(member, expected_members[i])
d01bb5
+
d01bb5
+    def test_primitive(self):
d01bb5
+        obj = relations.ResourcePrintableNode(D1_PRIMITIVE, [], False)
d01bb5
+        self.assertEqual(
d01bb5
+            "d1 (resource: ocf:pacemaker:Dummy)", obj.get_title(verbose=True)
d01bb5
+        )
d01bb5
+        self.assertEqual([], obj.detail)
d01bb5
+
d01bb5
+    def test_primitive_not_verbose(self):
d01bb5
+        obj = relations.ResourcePrintableNode(D1_PRIMITIVE, [], False)
d01bb5
+        self.assertEqual("d1", obj.get_title(verbose=False))
d01bb5
+        self.assertEqual([], obj.detail)
d01bb5
+
d01bb5
+    def test_primitive_without_provider_class(self):
d01bb5
+        obj = relations.ResourcePrintableNode(
d01bb5
+            RelationEntityDto(
d01bb5
+                "d1", "primitive", [], {
d01bb5
+                    "type": "Dummy",
d01bb5
+                }
d01bb5
+            ),
d01bb5
+            [],
d01bb5
+            False,
d01bb5
+        )
d01bb5
+        self.assertEqual("d1 (resource: Dummy)", obj.get_title(verbose=True))
d01bb5
+        self.assertEqual([], obj.detail)
d01bb5
+
d01bb5
+    def test_primitive_without_provider(self):
d01bb5
+        obj = relations.ResourcePrintableNode(
d01bb5
+            RelationEntityDto(
d01bb5
+                "d1", "primitive", [], {
d01bb5
+                    "class": "ocf",
d01bb5
+                    "type": "Dummy",
d01bb5
+                }
d01bb5
+            ),
d01bb5
+            [],
d01bb5
+            False,
d01bb5
+        )
d01bb5
+        self.assertEqual(
d01bb5
+            "d1 (resource: ocf:Dummy)", obj.get_title(verbose=True)
d01bb5
+        )
d01bb5
+        self.assertEqual([], obj.detail)
d01bb5
+
d01bb5
+    def test_primitive_without_class(self):
d01bb5
+        obj = relations.ResourcePrintableNode(
d01bb5
+            RelationEntityDto(
d01bb5
+                "d1", "primitive", [], {
d01bb5
+                    "provider": "pacemaker",
d01bb5
+                    "type": "Dummy",
d01bb5
+                }
d01bb5
+            ),
d01bb5
+            [],
d01bb5
+            False,
d01bb5
+        )
d01bb5
+        self.assertEqual(
d01bb5
+            "d1 (resource: pacemaker:Dummy)", obj.get_title(verbose=True)
d01bb5
+        )
d01bb5
+        self.assertEqual([], obj.detail)
d01bb5
+
d01bb5
+    def test_other(self):
d01bb5
+        obj = relations.ResourcePrintableNode(
d01bb5
+            RelationEntityDto("an_id", "a_type", [], {}), [], False,
d01bb5
+        )
d01bb5
+        self.assertEqual(
d01bb5
+            "an_id (resource: a_type)", obj.get_title(verbose=True)
d01bb5
+        )
d01bb5
+        self.assertEqual([], obj.detail)
d01bb5
+
d01bb5
+    def test_other_not_verbose(self):
d01bb5
+        obj = relations.ResourcePrintableNode(
d01bb5
+            RelationEntityDto("an_id", "a_type", [], {}), [], False,
d01bb5
+        )
d01bb5
+        self.assertEqual("an_id", obj.get_title(verbose=False))
d01bb5
+        self.assertEqual([], obj.detail)
d01bb5
+
d01bb5
+
d01bb5
+class RelationPrintableNode(TestCase):
d01bb5
+    def setUp(self):
d01bb5
+        self.order_entity = RelationEntityDto(
d01bb5
+            "order1", ResourceRelationType.ORDER, [], {
d01bb5
+                "id": "order1",
d01bb5
+                "first-action": "start",
d01bb5
+                "first": "d1",
d01bb5
+                "then-action": "start",
d01bb5
+                "then": "d2",
d01bb5
+            }
d01bb5
+        )
d01bb5
+        self.order_set_entity = RelationEntityDto(
d01bb5
+            "order_set_id", ResourceRelationType.ORDER_SET, [], {
d01bb5
+                "id": "order_set_id",
d01bb5
+                "sets": [
d01bb5
+                    {
d01bb5
+                        "members": ["d1", "d2", "d3"],
d01bb5
+                        "metadata": {},
d01bb5
+                    },
d01bb5
+                    {
d01bb5
+                        "members": ["d4", "d5", "d0"],
d01bb5
+                        "metadata": {
d01bb5
+                            "sequential": "true",
d01bb5
+                            "require-all": "false",
d01bb5
+                            "score": "10",
d01bb5
+                        },
d01bb5
+                    },
d01bb5
+                ],
d01bb5
+            }
d01bb5
+        )
d01bb5
+
d01bb5
+    def assert_member(self, member, ent):
d01bb5
+        self.assertTrue(isinstance(member, relations.ResourcePrintableNode))
d01bb5
+        self.assertEqual(ent, member.relation_entity)
d01bb5
+        self.assertEqual(True, member.is_leaf)
d01bb5
+        self.assertEqual(0, len(member.members))
d01bb5
+
d01bb5
+    def test_from_dto(self):
d01bb5
+        dto = ResourceRelationDto(
d01bb5
+            self.order_entity,
d01bb5
+            [
d01bb5
+                ResourceRelationDto(D2_PRIMITIVE, [], True),
d01bb5
+                ResourceRelationDto(D1_PRIMITIVE, [], True),
d01bb5
+            ],
d01bb5
+            False
d01bb5
+        )
d01bb5
+        obj = relations.RelationPrintableNode.from_dto(dto)
d01bb5
+        self.assertEqual(self.order_entity, obj.relation_entity)
d01bb5
+        self.assertEqual(False, obj.is_leaf)
d01bb5
+        self.assertEqual(2, len(obj.members))
d01bb5
+        self.assert_member(obj.members[0], D1_PRIMITIVE)
d01bb5
+        self.assert_member(obj.members[1], D2_PRIMITIVE)
d01bb5
+
d01bb5
+    def test_order_not_verbose(self):
d01bb5
+        obj = relations.RelationPrintableNode(self.order_entity, [], False)
d01bb5
+        self.assertEqual("order", obj.get_title(verbose=False))
d01bb5
+        self.assertEqual(["start d1 then start d2"], obj.detail)
d01bb5
+
d01bb5
+    def test_order(self):
d01bb5
+        obj = relations.RelationPrintableNode(self.order_entity, [], False)
d01bb5
+        self.assertEqual("order (order1)", obj.get_title(verbose=True))
d01bb5
+        self.assertEqual(["start d1 then start d2"], obj.detail)
d01bb5
+
d01bb5
+    def test_order_full(self):
d01bb5
+        self.order_entity.metadata.update({
d01bb5
+            "kind": "Optional",
d01bb5
+            "symmetrical": "true",
d01bb5
+            "unsupported": "value",
d01bb5
+            "score": "1000",
d01bb5
+        })
d01bb5
+        obj = relations.RelationPrintableNode(self.order_entity, [], False)
d01bb5
+        self.assertEqual("order (order1)", obj.get_title(verbose=True))
d01bb5
+        self.assertEqual(
d01bb5
+            [
d01bb5
+                "start d1 then start d2",
d01bb5
+                "kind=Optional score=1000 symmetrical=true"
d01bb5
+            ],
d01bb5
+            obj.detail,
d01bb5
+        )
d01bb5
+
d01bb5
+    def test_order_set_not_verbose(self):
d01bb5
+        obj = relations.RelationPrintableNode(self.order_set_entity, [], False)
d01bb5
+        self.assertEqual("order set", obj.get_title(verbose=False))
d01bb5
+        self.assertEqual(
d01bb5
+            [
d01bb5
+                "   set d1 d2 d3",
d01bb5
+                "   set d4 d5 d0 (require-all=false score=10 sequential=true)",
d01bb5
+            ],
d01bb5
+            obj.detail,
d01bb5
+        )
d01bb5
+
d01bb5
+    def test_order_set(self):
d01bb5
+        obj = relations.RelationPrintableNode(self.order_set_entity, [], False)
d01bb5
+        self.assertEqual(
d01bb5
+            "order set (order_set_id)", obj.get_title(verbose=True)
d01bb5
+        )
d01bb5
+        self.assertEqual(
d01bb5
+            [
d01bb5
+                "   set d1 d2 d3",
d01bb5
+                "   set d4 d5 d0 (require-all=false score=10 sequential=true)",
d01bb5
+            ],
d01bb5
+            obj.detail,
d01bb5
+        )
d01bb5
+
d01bb5
+    def test_order_set_full(self):
d01bb5
+        self.order_set_entity.metadata.update({
d01bb5
+            "symmetrical": "true",
d01bb5
+            "kind": "Optional",
d01bb5
+            "require-all": "true",
d01bb5
+            "score": "100",
d01bb5
+            "unsupported": "value",
d01bb5
+        })
d01bb5
+        self.order_set_entity.metadata["sets"].append({
d01bb5
+            "members": ["d9", "d8", "d6", "d7"],
d01bb5
+            "metadata": {
d01bb5
+                "sequential": "true",
d01bb5
+                "require-all": "false",
d01bb5
+                "score": "10",
d01bb5
+                "ordering": "value",
d01bb5
+                "action": "start",
d01bb5
+                "role": "promoted",
d01bb5
+                "kind": "Optional",
d01bb5
+                "unsupported": "value",
d01bb5
+            },
d01bb5
+        })
d01bb5
+        obj = relations.RelationPrintableNode(self.order_set_entity, [], False)
d01bb5
+        self.assertEqual(
d01bb5
+            "order set (order_set_id)", obj.get_title(verbose=True)
d01bb5
+        )
d01bb5
+        self.assertEqual(
d01bb5
+            [
d01bb5
+                "kind=Optional require-all=true score=100 symmetrical=true",
d01bb5
+                "   set d1 d2 d3",
d01bb5
+                "   set d4 d5 d0 (require-all=false score=10 sequential=true)",
d01bb5
+                "   set d9 d8 d6 d7 (action=start kind=Optional ordering=value "
d01bb5
+                "require-all=false role=promoted score=10 sequential=true)",
d01bb5
+            ],
d01bb5
+            obj.detail,
d01bb5
+        )
d01bb5
+
d01bb5
+    def test_multiple_inner_resources(self):
d01bb5
+        obj = relations.RelationPrintableNode(
d01bb5
+            RelationEntityDto(
d01bb5
+                "inner:g1",
d01bb5
+                ResourceRelationType.INNER_RESOURCES,
d01bb5
+                ["m1", "m2", "m0"],
d01bb5
+                {"id": "g1"}
d01bb5
+            ),
d01bb5
+            [],
d01bb5
+            False,
d01bb5
+        )
d01bb5
+        self.assertEqual("inner resource(s) (g1)", obj.get_title(verbose=True))
d01bb5
+        self.assertEqual(["members: m1 m2 m0"], obj.detail)
d01bb5
+
d01bb5
+    def test_inner_resources_not_verbose(self):
d01bb5
+        obj = relations.RelationPrintableNode(
d01bb5
+            RelationEntityDto(
d01bb5
+                "inner:g1", ResourceRelationType.INNER_RESOURCES, ["m0"], {
d01bb5
+                    "id": "g1",
d01bb5
+                }
d01bb5
+            ),
d01bb5
+            [],
d01bb5
+            False,
d01bb5
+        )
d01bb5
+        self.assertEqual("inner resource(s)", obj.get_title(verbose=False))
d01bb5
+        self.assertEqual([], obj.detail)
d01bb5
+
d01bb5
+    def test_inner_resources(self):
d01bb5
+        obj = relations.RelationPrintableNode(
d01bb5
+            RelationEntityDto(
d01bb5
+                "inner:g1", ResourceRelationType.INNER_RESOURCES, ["m0"], {
d01bb5
+                    "id": "g1",
d01bb5
+                }
d01bb5
+            ),
d01bb5
+            [],
d01bb5
+            False,
d01bb5
+        )
d01bb5
+        self.assertEqual("inner resource(s) (g1)", obj.get_title(verbose=True))
d01bb5
+        self.assertEqual([], obj.detail)
d01bb5
+
d01bb5
+    def test_outer_resourcenot_verbose(self):
d01bb5
+        obj = relations.RelationPrintableNode(
d01bb5
+            RelationEntityDto(
d01bb5
+                "outer:g1", ResourceRelationType.OUTER_RESOURCE, [], {
d01bb5
+                    "id": "g1",
d01bb5
+                }
d01bb5
+            ),
d01bb5
+            [],
d01bb5
+            False,
d01bb5
+        )
d01bb5
+        self.assertEqual("outer resource", obj.get_title(verbose=False))
d01bb5
+        self.assertEqual([], obj.detail)
d01bb5
+
d01bb5
+    def test_outer_resource(self):
d01bb5
+        obj = relations.RelationPrintableNode(
d01bb5
+            RelationEntityDto(
d01bb5
+                "outer:g1", ResourceRelationType.OUTER_RESOURCE, [], {
d01bb5
+                    "id": "g1",
d01bb5
+                }
d01bb5
+            ),
d01bb5
+            [],
d01bb5
+            False,
d01bb5
+        )
d01bb5
+        self.assertEqual("outer resource (g1)", obj.get_title(verbose=True))
d01bb5
+        self.assertEqual([], obj.detail)
d01bb5
+
d01bb5
+    def test_unknown_not_verbose(self):
d01bb5
+        obj = relations.RelationPrintableNode(
d01bb5
+            RelationEntityDto(
d01bb5
+                "random", "undifined type", [], {
d01bb5
+                    "id": "random_id",
d01bb5
+                }
d01bb5
+            ),
d01bb5
+            [],
d01bb5
+            False,
d01bb5
+        )
d01bb5
+        self.assertEqual("<unknown>", obj.get_title(verbose=False))
d01bb5
+        self.assertEqual([], obj.detail)
d01bb5
+
d01bb5
+    def test_unknown(self):
d01bb5
+        obj = relations.RelationPrintableNode(
d01bb5
+            RelationEntityDto(
d01bb5
+                "random", "undifined type", [], {
d01bb5
+                    "id": "random_id",
d01bb5
+                }
d01bb5
+            ),
d01bb5
+            [],
d01bb5
+            False,
d01bb5
+        )
d01bb5
+        self.assertEqual("<unknown> (random_id)", obj.get_title(verbose=True))
d01bb5
+        self.assertEqual([], obj.detail)
d01bb5
diff --git a/pcs/common/interface/__init__.py b/pcs/common/interface/__init__.py
d01bb5
new file mode 100644
d01bb5
index 00000000..e69de29b
d01bb5
diff --git a/pcs/common/interface/dto.py b/pcs/common/interface/dto.py
d01bb5
new file mode 100644
d01bb5
index 00000000..b95367fd
d01bb5
--- /dev/null
d01bb5
+++ b/pcs/common/interface/dto.py
d01bb5
@@ -0,0 +1,18 @@
d01bb5
+class DataTransferObject(object):
d01bb5
+    def to_dict(self):
d01bb5
+        raise NotImplementedError()
d01bb5
+
d01bb5
+    @classmethod
d01bb5
+    def from_dict(cls, payload):
d01bb5
+        raise NotImplementedError()
d01bb5
+
d01bb5
+
d01bb5
+class ImplementsToDto(object):
d01bb5
+    def to_dto(self):
d01bb5
+        raise NotImplementedError()
d01bb5
+
d01bb5
+
d01bb5
+class ImplementsFromDto(object):
d01bb5
+    @classmethod
d01bb5
+    def from_dto(cls, dto_obj):
d01bb5
+        raise NotImplementedError()
d01bb5
diff --git a/pcs/common/pacemaker/__init__.py b/pcs/common/pacemaker/__init__.py
d01bb5
new file mode 100644
d01bb5
index 00000000..e69de29b
d01bb5
diff --git a/pcs/common/pacemaker/resource/__init__.py b/pcs/common/pacemaker/resource/__init__.py
d01bb5
new file mode 100644
d01bb5
index 00000000..e69de29b
d01bb5
diff --git a/pcs/common/pacemaker/resource/relations.py b/pcs/common/pacemaker/resource/relations.py
d01bb5
new file mode 100644
d01bb5
index 00000000..c074a212
d01bb5
--- /dev/null
d01bb5
+++ b/pcs/common/pacemaker/resource/relations.py
d01bb5
@@ -0,0 +1,66 @@
d01bb5
+from pcs.common.interface.dto import DataTransferObject
d01bb5
+
d01bb5
+
d01bb5
+class ResourceRelationType(object):
d01bb5
+    ORDER = "ORDER"
d01bb5
+    ORDER_SET = "ORDER_SET"
d01bb5
+    INNER_RESOURCES = "INNER_RESOURCES"
d01bb5
+    OUTER_RESOURCE = "OUTER_RESOURCE"
d01bb5
+
d01bb5
+
d01bb5
+class RelationEntityDto(DataTransferObject):
d01bb5
+    def __init__(self, id_, type_, members, metadata):
d01bb5
+        # pylint: disable=invalid-name
d01bb5
+        self.id = id_
d01bb5
+        self.type = type_
d01bb5
+        self.members = members
d01bb5
+        self.metadata = metadata
d01bb5
+
d01bb5
+    def __eq__(self, other):
d01bb5
+        return (
d01bb5
+            isinstance(other, self.__class__)
d01bb5
+            and
d01bb5
+            self.to_dict() == other.to_dict()
d01bb5
+        )
d01bb5
+
d01bb5
+    def to_dict(self):
d01bb5
+        return dict(
d01bb5
+            id=self.id,
d01bb5
+            type=self.type,
d01bb5
+            members=self.members,
d01bb5
+            metadata=self.metadata,
d01bb5
+        )
d01bb5
+
d01bb5
+    @classmethod
d01bb5
+    def from_dict(cls, payload):
d01bb5
+        return cls(
d01bb5
+            payload["id"],
d01bb5
+            payload["type"],
d01bb5
+            payload["members"],
d01bb5
+            payload["metadata"],
d01bb5
+        )
d01bb5
+
d01bb5
+
d01bb5
+class ResourceRelationDto(DataTransferObject):
d01bb5
+    def __init__(self, relation_entity, members, is_leaf):
d01bb5
+        self.relation_entity = relation_entity
d01bb5
+        self.members = members
d01bb5
+        self.is_leaf = is_leaf
d01bb5
+
d01bb5
+    def to_dict(self):
d01bb5
+        return dict(
d01bb5
+            relation_entity=self.relation_entity.to_dict(),
d01bb5
+            members=[member.to_dict() for member in self.members],
d01bb5
+            is_leaf=self.is_leaf,
d01bb5
+        )
d01bb5
+
d01bb5
+    @classmethod
d01bb5
+    def from_dict(cls, payload):
d01bb5
+        return cls(
d01bb5
+            RelationEntityDto.from_dict(payload["relation_entity"]),
d01bb5
+            [
d01bb5
+                ResourceRelationDto.from_dict(member_data)
d01bb5
+                for member_data in payload["members"]
d01bb5
+            ],
d01bb5
+            payload["is_leaf"],
d01bb5
+        )
d01bb5
diff --git a/pcs/common/pacemaker/resource/test/__init__.py b/pcs/common/pacemaker/resource/test/__init__.py
d01bb5
new file mode 100644
d01bb5
index 00000000..e69de29b
d01bb5
diff --git a/pcs/common/pacemaker/resource/test/test_relations.py b/pcs/common/pacemaker/resource/test/test_relations.py
d01bb5
new file mode 100644
d01bb5
index 00000000..19127ddd
d01bb5
--- /dev/null
d01bb5
+++ b/pcs/common/pacemaker/resource/test/test_relations.py
d01bb5
@@ -0,0 +1,169 @@
d01bb5
+from pcs.test.tools.pcs_unittest import TestCase
d01bb5
+
d01bb5
+from pcs.common.pacemaker.resource import relations
d01bb5
+
d01bb5
+
d01bb5
+class RelationEntityDto(TestCase):
d01bb5
+    def setUp(self):
d01bb5
+        self.an_id = "an_id"
d01bb5
+        self.members = ["m2", "m1", "m3", "m0"]
d01bb5
+        self.metadata = dict(
d01bb5
+            key1="val1",
d01bb5
+            key0="vel0",
d01bb5
+            keyx="valx",
d01bb5
+        )
d01bb5
+        self.str_type = "a_type"
d01bb5
+        self.enum_type = relations.ResourceRelationType.ORDER_SET
d01bb5
+
d01bb5
+    def dto_fixture(self, a_type):
d01bb5
+        return relations.RelationEntityDto(
d01bb5
+            self.an_id, a_type, self.members, self.metadata
d01bb5
+        )
d01bb5
+
d01bb5
+    def dict_fixture(self, a_type):
d01bb5
+        return dict(
d01bb5
+            id=self.an_id,
d01bb5
+            type=a_type,
d01bb5
+            members=self.members,
d01bb5
+            metadata=self.metadata,
d01bb5
+        )
d01bb5
+
d01bb5
+    def test_to_dict_type_str(self):
d01bb5
+        dto = self.dto_fixture(self.str_type)
d01bb5
+        self.assertEqual(self.dict_fixture(self.str_type), dto.to_dict())
d01bb5
+
d01bb5
+    def test_to_dict_type_enum(self):
d01bb5
+        dto = self.dto_fixture(self.enum_type)
d01bb5
+        self.assertEqual(self.dict_fixture(self.enum_type), dto.to_dict())
d01bb5
+
d01bb5
+    def test_from_dict_type_str(self):
d01bb5
+        dto = relations.RelationEntityDto.from_dict(
d01bb5
+            self.dict_fixture(self.str_type)
d01bb5
+        )
d01bb5
+        self.assertEqual(dto.id, self.an_id)
d01bb5
+        self.assertEqual(dto.type, self.str_type)
d01bb5
+        self.assertEqual(dto.members, self.members)
d01bb5
+        self.assertEqual(dto.metadata, self.metadata)
d01bb5
+
d01bb5
+    def test_from_dict_type_enum(self):
d01bb5
+        dto = relations.RelationEntityDto.from_dict(
d01bb5
+            self.dict_fixture(self.enum_type)
d01bb5
+        )
d01bb5
+        self.assertEqual(dto.id, self.an_id)
d01bb5
+        self.assertEqual(dto.type, self.enum_type)
d01bb5
+        self.assertEqual(dto.members, self.members)
d01bb5
+        self.assertEqual(dto.metadata, self.metadata)
d01bb5
+
d01bb5
+
d01bb5
+class ResourceRelationDto(TestCase):
d01bb5
+    @staticmethod
d01bb5
+    def ent_dto_fixture(an_id):
d01bb5
+        return relations.RelationEntityDto(
d01bb5
+            an_id, "a_type", ["m1", "m0", "m2"], dict(k1="v1", kx="vx")
d01bb5
+        )
d01bb5
+
d01bb5
+    def test_to_dict_no_members(self):
d01bb5
+        ent_dto = self.ent_dto_fixture("an_id")
d01bb5
+        dto = relations.ResourceRelationDto(ent_dto, [], True)
d01bb5
+        self.assertEqual(
d01bb5
+            dict(
d01bb5
+                relation_entity=ent_dto.to_dict(),
d01bb5
+                members=[],
d01bb5
+                is_leaf=True,
d01bb5
+            ),
d01bb5
+            dto.to_dict()
d01bb5
+        )
d01bb5
+
d01bb5
+    def test_to_dict_with_members(self):
d01bb5
+        ent_dto = self.ent_dto_fixture("an_id")
d01bb5
+        m1_ent = self.ent_dto_fixture("m1_ent")
d01bb5
+        m2_ent = self.ent_dto_fixture("m2_ent")
d01bb5
+        m3_ent = self.ent_dto_fixture("m3_ent")
d01bb5
+        members = [
d01bb5
+            relations.ResourceRelationDto(
d01bb5
+                m1_ent, [], False
d01bb5
+            ),
d01bb5
+            relations.ResourceRelationDto(
d01bb5
+                m2_ent,
d01bb5
+                [relations.ResourceRelationDto(m3_ent, [], True)],
d01bb5
+                False
d01bb5
+            ),
d01bb5
+        ]
d01bb5
+        dto = relations.ResourceRelationDto(ent_dto, members, True)
d01bb5
+        self.assertEqual(
d01bb5
+            dict(
d01bb5
+                relation_entity=ent_dto.to_dict(),
d01bb5
+                members=[
d01bb5
+                    dict(
d01bb5
+                        relation_entity=m1_ent.to_dict(),
d01bb5
+                        members=[],
d01bb5
+                        is_leaf=False,
d01bb5
+                    ),
d01bb5
+                    dict(
d01bb5
+                        relation_entity=m2_ent.to_dict(),
d01bb5
+                        members=[
d01bb5
+                            dict(
d01bb5
+                                relation_entity=m3_ent.to_dict(),
d01bb5
+                                members=[],
d01bb5
+                                is_leaf=True,
d01bb5
+                            )
d01bb5
+                        ],
d01bb5
+                        is_leaf=False,
d01bb5
+                    ),
d01bb5
+                ],
d01bb5
+                is_leaf=True,
d01bb5
+            ),
d01bb5
+            dto.to_dict()
d01bb5
+        )
d01bb5
+
d01bb5
+    def test_from_dict(self):
d01bb5
+        ent_dto = self.ent_dto_fixture("an_id")
d01bb5
+        m1_ent = self.ent_dto_fixture("m1_ent")
d01bb5
+        m2_ent = self.ent_dto_fixture("m2_ent")
d01bb5
+        m3_ent = self.ent_dto_fixture("m3_ent")
d01bb5
+        dto = relations.ResourceRelationDto.from_dict(
d01bb5
+            dict(
d01bb5
+                relation_entity=ent_dto.to_dict(),
d01bb5
+                members=[
d01bb5
+                    dict(
d01bb5
+                        relation_entity=m1_ent.to_dict(),
d01bb5
+                        members=[],
d01bb5
+                        is_leaf=False,
d01bb5
+                    ),
d01bb5
+                    dict(
d01bb5
+                        relation_entity=m2_ent.to_dict(),
d01bb5
+                        members=[
d01bb5
+                            dict(
d01bb5
+                                relation_entity=m3_ent.to_dict(),
d01bb5
+                                members=[],
d01bb5
+                                is_leaf=True,
d01bb5
+                            )
d01bb5
+                        ],
d01bb5
+                        is_leaf=False,
d01bb5
+                    ),
d01bb5
+                ],
d01bb5
+                is_leaf=True,
d01bb5
+            )
d01bb5
+        )
d01bb5
+        self.assertEqual(ent_dto.to_dict(), dto.relation_entity.to_dict())
d01bb5
+        self.assertEqual(True, dto.is_leaf)
d01bb5
+        self.assertEqual(2, len(dto.members))
d01bb5
+
d01bb5
+        self.assertEqual(
d01bb5
+            m1_ent.to_dict(), dto.members[0].relation_entity.to_dict()
d01bb5
+        )
d01bb5
+        self.assertEqual(False, dto.members[0].is_leaf)
d01bb5
+        self.assertEqual(0, len(dto.members[0].members))
d01bb5
+
d01bb5
+        self.assertEqual(
d01bb5
+            m2_ent.to_dict(), dto.members[1].relation_entity.to_dict()
d01bb5
+        )
d01bb5
+        self.assertEqual(False, dto.members[1].is_leaf)
d01bb5
+        self.assertEqual(1, len(dto.members[1].members))
d01bb5
+
d01bb5
+        self.assertEqual(
d01bb5
+            m3_ent.to_dict(),
d01bb5
+            dto.members[1].members[0].relation_entity.to_dict(),
d01bb5
+        )
d01bb5
+        self.assertEqual(True, dto.members[1].members[0].is_leaf)
d01bb5
+        self.assertEqual(0, len(dto.members[1].members[0].members))
d01bb5
diff --git a/pcs/lib/cib/resource/__init__.py b/pcs/lib/cib/resource/__init__.py
d01bb5
index 620af424..dedc03af 100644
d01bb5
--- a/pcs/lib/cib/resource/__init__.py
d01bb5
+++ b/pcs/lib/cib/resource/__init__.py
d01bb5
@@ -12,5 +12,6 @@ from pcs.lib.cib.resource import (
d01bb5
     guest_node,
d01bb5
     operations,
d01bb5
     primitive,
d01bb5
+    relations,
d01bb5
     remote_node,
d01bb5
 )
d01bb5
diff --git a/pcs/lib/cib/resource/common.py b/pcs/lib/cib/resource/common.py
d01bb5
index cc4bb1d0..87b656b7 100644
d01bb5
--- a/pcs/lib/cib/resource/common.py
d01bb5
+++ b/pcs/lib/cib/resource/common.py
d01bb5
@@ -51,6 +51,54 @@ def find_primitives(resource_el):
d01bb5
         return [resource_el]
d01bb5
     return []
d01bb5
 
d01bb5
+def get_inner_resources(resource_el):
d01bb5
+    """
d01bb5
+    Return list of inner resources (direct descendants) of a resource
d01bb5
+    specified as resource_el.
d01bb5
+    Example: for clone containing a group, this function will return only
d01bb5
+    group and not resource inside the group
d01bb5
+
d01bb5
+    resource_el -- resource element to get its inner resources
d01bb5
+    """
d01bb5
+    if is_bundle(resource_el):
d01bb5
+        in_bundle = get_bundle_inner_resource(resource_el)
d01bb5
+        return [in_bundle] if in_bundle is not None else []
d01bb5
+    if is_any_clone(resource_el):
d01bb5
+        return [get_clone_inner_resource(resource_el)]
d01bb5
+    if is_group(resource_el):
d01bb5
+        return get_group_inner_resources(resource_el)
d01bb5
+    return []
d01bb5
+
d01bb5
+def is_wrapper_resource(resource_el):
d01bb5
+    """
d01bb5
+    Return True for resource_el of types that can contain other resource(s)
d01bb5
+    (these are: group, bundle, clone) and False otherwise.
d01bb5
+
d01bb5
+    resource_el -- resource element to check
d01bb5
+    """
d01bb5
+    return (
d01bb5
+        is_group(resource_el)
d01bb5
+        or
d01bb5
+        is_bundle(resource_el)
d01bb5
+        or
d01bb5
+        is_any_clone(resource_el)
d01bb5
+    )
d01bb5
+
d01bb5
+def get_parent_resource(resource_el):
d01bb5
+    """
d01bb5
+    Return a direct ancestor of a specified resource or None if the resource
d01bb5
+    has no ancestor.
d01bb5
+    Example: for a resource in group which is in clone, this function will
d01bb5
+    return group element.
d01bb5
+
d01bb5
+    resource_el -- resource element of which parent resource should be returned
d01bb5
+    """
d01bb5
+    parent_el = resource_el.getparent()
d01bb5
+    if parent_el is not None and is_wrapper_resource(parent_el):
d01bb5
+        return parent_el
d01bb5
+    return None
d01bb5
+
d01bb5
+
d01bb5
 def find_resources_to_enable(resource_el):
d01bb5
     """
d01bb5
     Get resources to enable in order to enable specified resource succesfully
d01bb5
diff --git a/pcs/lib/cib/resource/relations.py b/pcs/lib/cib/resource/relations.py
d01bb5
new file mode 100644
d01bb5
index 00000000..7935336a
d01bb5
--- /dev/null
d01bb5
+++ b/pcs/lib/cib/resource/relations.py
d01bb5
@@ -0,0 +1,265 @@
d01bb5
+from pcs.common.pacemaker.resource.relations import (
d01bb5
+    RelationEntityDto,
d01bb5
+    ResourceRelationDto,
d01bb5
+    ResourceRelationType,
d01bb5
+)
d01bb5
+from pcs.lib.cib import tools
d01bb5
+from pcs.lib.cib.resource import common
d01bb5
+
d01bb5
+from pcs.lib.cib.resource.bundle import TAG as TAG_BUNDLE
d01bb5
+from pcs.lib.cib.resource.clone import ALL_TAGS as TAG_CLONE_ALL
d01bb5
+from pcs.lib.cib.resource.group import TAG as TAG_GROUP
d01bb5
+from pcs.lib.cib.resource.primitive import TAG as TAG_PRIMITIVE
d01bb5
+
d01bb5
+
d01bb5
+# character ':' ensures that there is no conflict with any id in CIB, as it
d01bb5
+# would be an invalid id
d01bb5
+INNER_RESOURCE_ID_TEMPLATE = "inner:{}"
d01bb5
+OUTER_RESOURCE_ID_TEMPLATE = "outer:{}"
d01bb5
+
d01bb5
+
d01bb5
+def _get_opposite_relation_id_template(relation_type):
d01bb5
+    return {
d01bb5
+        ResourceRelationType.INNER_RESOURCES: OUTER_RESOURCE_ID_TEMPLATE,
d01bb5
+        ResourceRelationType.OUTER_RESOURCE: INNER_RESOURCE_ID_TEMPLATE,
d01bb5
+    }.get(relation_type, "")
d01bb5
+
d01bb5
+
d01bb5
+class ResourceRelationNode(object):
d01bb5
+    def __init__(self, entity):
d01bb5
+        self._obj = entity
d01bb5
+        self._members = []
d01bb5
+        self._is_leaf = False
d01bb5
+        self._parent = None
d01bb5
+        self._opposite_id = _get_opposite_relation_id_template(
d01bb5
+            self._obj.type
d01bb5
+        ).format(self._obj.metadata["id"])
d01bb5
+
d01bb5
+    @property
d01bb5
+    def obj(self):
d01bb5
+        return self._obj
d01bb5
+
d01bb5
+    @property
d01bb5
+    def members(self):
d01bb5
+        return self._members
d01bb5
+
d01bb5
+    def to_dto(self):
d01bb5
+        return ResourceRelationDto(
d01bb5
+            self._obj,
d01bb5
+            [member.to_dto() for member in self._members],
d01bb5
+            self._is_leaf,
d01bb5
+        )
d01bb5
+
d01bb5
+    def stop(self):
d01bb5
+        self._is_leaf = True
d01bb5
+
d01bb5
+    def add_member(self, member):
d01bb5
+        # pylint: disable=protected-access
d01bb5
+        if member._parent is not None:
d01bb5
+            raise AssertionError(
d01bb5
+                "object {} already has a parent set: {}".format(
d01bb5
+                    repr(member), repr(member._parent)
d01bb5
+                )
d01bb5
+            )
d01bb5
+        # we don't want opposite relations (inner resource vs outer resource)
d01bb5
+        # in a branch, so we are filtering them out
d01bb5
+        parents = set(self._get_all_parents())
d01bb5
+        if (
d01bb5
+            self != member
d01bb5
+            and
d01bb5
+            member.obj.id not in parents
d01bb5
+            and
d01bb5
+            (
d01bb5
+                member._opposite_id not in parents
d01bb5
+                or
d01bb5
+                len(member.obj.members) > 1
d01bb5
+            )
d01bb5
+        ):
d01bb5
+            member._parent = self
d01bb5
+            self._members.append(member)
d01bb5
+
d01bb5
+    def _get_all_parents(self):
d01bb5
+        # pylint: disable=protected-access
d01bb5
+        if self._parent is None:
d01bb5
+            return []
d01bb5
+        return self._parent._get_all_parents() + [self._parent.obj.id]
d01bb5
+
d01bb5
+
d01bb5
+class ResourceRelationTreeBuilder(object):
d01bb5
+    def __init__(self, resource_entities, relation_entities):
d01bb5
+        self._resources = resource_entities
d01bb5
+        self._all = dict(resource_entities)
d01bb5
+        self._all.update(relation_entities)
d01bb5
+        self._init_structures()
d01bb5
+
d01bb5
+    def _init_structures(self):
d01bb5
+        self._processed_nodes = set()
d01bb5
+        # queue
d01bb5
+        self._nodes_to_process = []
d01bb5
+
d01bb5
+    def get_tree(self, resource_id):
d01bb5
+        self._init_structures()
d01bb5
+        if resource_id not in self._resources:
d01bb5
+            raise AssertionError(
d01bb5
+                "Resource with id '{}' not found in resource "
d01bb5
+                "relation structures".format(resource_id)
d01bb5
+            )
d01bb5
+
d01bb5
+        # self._all is a superset of self._resources, see __init__
d01bb5
+        root = ResourceRelationNode(self._all[resource_id])
d01bb5
+        self._nodes_to_process.append(root)
d01bb5
+
d01bb5
+        while self._nodes_to_process:
d01bb5
+            node = self._nodes_to_process.pop(0)
d01bb5
+            if node.obj.id in self._processed_nodes:
d01bb5
+                node.stop()
d01bb5
+                continue
d01bb5
+            self._processed_nodes.add(node.obj.id)
d01bb5
+            for node_id in node.obj.members:
d01bb5
+                node.add_member(
d01bb5
+                    ResourceRelationNode(self._all[node_id])
d01bb5
+                )
d01bb5
+            self._nodes_to_process.extend(node.members)
d01bb5
+        return root
d01bb5
+
d01bb5
+
d01bb5
+class ResourceRelationsFetcher(object):
d01bb5
+    def __init__(self, cib):
d01bb5
+        self._cib = cib
d01bb5
+        self._resources_section = tools.get_resources(self._cib)
d01bb5
+        self._constraints_section = tools.get_constraints(self._cib)
d01bb5
+
d01bb5
+    def get_relations(self, resource_id):
d01bb5
+        resources_to_process = {resource_id}
d01bb5
+        relations = {}
d01bb5
+        resources = {}
d01bb5
+        while resources_to_process:
d01bb5
+            res_id = resources_to_process.pop()
d01bb5
+            if res_id in resources:
d01bb5
+                # already processed
d01bb5
+                continue
d01bb5
+            res_el = self._get_resource_el(res_id)
d01bb5
+            res_relations = {
d01bb5
+                rel.id: rel for rel in self._get_resource_relations(res_el)
d01bb5
+            }
d01bb5
+            resources[res_id] = RelationEntityDto(
d01bb5
+                res_id,
d01bb5
+                res_el.tag,
d01bb5
+                list(res_relations.keys()),
d01bb5
+                dict(res_el.attrib),
d01bb5
+            )
d01bb5
+            relations.update(res_relations)
d01bb5
+            resources_to_process.update(
d01bb5
+                self._get_all_members(res_relations.values())
d01bb5
+            )
d01bb5
+        return resources, relations
d01bb5
+
d01bb5
+    def _get_resource_el(self, res_id):
d01bb5
+        # client of this class should ensure that res_id really exists in CIB,
d01bb5
+        # so here we don't need to handle possible reports
d01bb5
+        for tag in TAG_CLONE_ALL + [TAG_GROUP, TAG_PRIMITIVE, TAG_BUNDLE]:
d01bb5
+            element_list = self._resources_section.xpath(
d01bb5
+                './/{}[@id="{}"]'.format(tag, res_id)
d01bb5
+            )
d01bb5
+            if element_list:
d01bb5
+                return element_list[0]
d01bb5
+
d01bb5
+    @staticmethod
d01bb5
+    def _get_all_members(relation_list):
d01bb5
+        result = set()
d01bb5
+        for relation in relation_list:
d01bb5
+            result.update(relation.members)
d01bb5
+        return result
d01bb5
+
d01bb5
+    def _get_resource_relations(self, resource_el):
d01bb5
+        resource_id = resource_el.attrib["id"]
d01bb5
+        relations = [
d01bb5
+            _get_ordering_constraint_relation(item)
d01bb5
+            for item in self._get_ordering_coinstraints(resource_id)
d01bb5
+        ] + [
d01bb5
+            _get_ordering_set_constraint_relation(item)
d01bb5
+            for item in self._get_ordering_set_constraints(resource_id)
d01bb5
+        ]
d01bb5
+
d01bb5
+        # special type of relation, group (note that a group can be a resource
d01bb5
+        # and a relation)
d01bb5
+        if common.is_wrapper_resource(resource_el):
d01bb5
+            relations.append(_get_inner_resources_relation(resource_el))
d01bb5
+
d01bb5
+        # handle resources in a wrapper resource (group/bundle/clone relation)
d01bb5
+        parent_el = common.get_parent_resource(resource_el)
d01bb5
+        if parent_el is not None:
d01bb5
+            relations.append(_get_outer_resource_relation(parent_el))
d01bb5
+        return relations
d01bb5
+
d01bb5
+    def _get_ordering_coinstraints(self, resource_id):
d01bb5
+        return self._constraints_section.xpath("""
d01bb5
+            .//rsc_order[
d01bb5
+                not (descendant::resource_set)
d01bb5
+                and
d01bb5
+                (@first='{0}' or @then='{0}')
d01bb5
+            ]
d01bb5
+        """.format(resource_id))
d01bb5
+
d01bb5
+    def _get_ordering_set_constraints(self, resource_id):
d01bb5
+        return self._constraints_section.xpath(
d01bb5
+            ".//rsc_order[./resource_set/resource_ref[@id='{}']]".format(
d01bb5
+                resource_id
d01bb5
+            )
d01bb5
+        )
d01bb5
+
d01bb5
+
d01bb5
+# relation obj to RelationEntityDto obj
d01bb5
+def _get_inner_resources_relation(parent_resource_el):
d01bb5
+    attrs = parent_resource_el.attrib
d01bb5
+    return RelationEntityDto(
d01bb5
+        INNER_RESOURCE_ID_TEMPLATE.format(attrs["id"]),
d01bb5
+        ResourceRelationType.INNER_RESOURCES,
d01bb5
+        [
d01bb5
+            res.attrib["id"]
d01bb5
+            for res in common.get_inner_resources(parent_resource_el)
d01bb5
+        ],
d01bb5
+        dict(attrs),
d01bb5
+    )
d01bb5
+
d01bb5
+
d01bb5
+def _get_outer_resource_relation(parent_resource_el):
d01bb5
+    attrs = parent_resource_el.attrib
d01bb5
+    return RelationEntityDto(
d01bb5
+        OUTER_RESOURCE_ID_TEMPLATE.format(attrs["id"]),
d01bb5
+        ResourceRelationType.OUTER_RESOURCE,
d01bb5
+        [attrs["id"]],
d01bb5
+        dict(attrs),
d01bb5
+    )
d01bb5
+
d01bb5
+
d01bb5
+def _get_ordering_constraint_relation(ord_const_el):
d01bb5
+    attrs = ord_const_el.attrib
d01bb5
+    return RelationEntityDto(
d01bb5
+        attrs["id"],
d01bb5
+        ResourceRelationType.ORDER,
d01bb5
+        [attrs["first"], attrs["then"]],
d01bb5
+        dict(attrs),
d01bb5
+    )
d01bb5
+
d01bb5
+
d01bb5
+def _get_ordering_set_constraint_relation(ord_set_const_el):
d01bb5
+    attrs = ord_set_const_el.attrib
d01bb5
+    members = set()
d01bb5
+    metadata = dict(attrs)
d01bb5
+    metadata["sets"] = []
d01bb5
+    for rsc_set_el in ord_set_const_el.findall("resource_set"):
d01bb5
+        rsc_set = dict(
d01bb5
+            id=rsc_set_el.get("id"),
d01bb5
+            metadata=dict(rsc_set_el.attrib),
d01bb5
+            members=[],
d01bb5
+        )
d01bb5
+        metadata["sets"].append(rsc_set)
d01bb5
+        for rsc_ref in rsc_set_el.findall("resource_ref"):
d01bb5
+            rsc_id = rsc_ref.attrib["id"]
d01bb5
+            members.add(rsc_id)
d01bb5
+            rsc_set["members"].append(rsc_id)
d01bb5
+
d01bb5
+    return RelationEntityDto(
d01bb5
+        attrs["id"], ResourceRelationType.ORDER_SET, sorted(members), metadata
d01bb5
+    )
d01bb5
diff --git a/pcs/lib/cib/test/test_resource_common.py b/pcs/lib/cib/test/test_resource_common.py
d01bb5
index 21380060..596ee57f 100644
d01bb5
--- a/pcs/lib/cib/test/test_resource_common.py
d01bb5
+++ b/pcs/lib/cib/test/test_resource_common.py
d01bb5
@@ -83,14 +83,14 @@ class IsCloneDeactivatedByMeta(TestCase):
d01bb5
         self.assert_is_not_disabled({"clone-node-max": "1.1"})
d01bb5
 
d01bb5
 
d01bb5
-class FindPrimitives(TestCase):
d01bb5
+class FindResourcesMixin(object):
d01bb5
     def assert_find_resources(self, input_resource_id, output_resource_ids):
d01bb5
         self.assertEqual(
d01bb5
             output_resource_ids,
d01bb5
             [
d01bb5
                 element.get("id", "")
d01bb5
                 for element in
d01bb5
-                common.find_primitives(
d01bb5
+                self._tested_fn(
d01bb5
                     fixture_cib.find(
d01bb5
                         './/*[@id="{0}"]'.format(input_resource_id)
d01bb5
                     )
d01bb5
@@ -98,6 +98,31 @@ class FindPrimitives(TestCase):
d01bb5
             ]
d01bb5
         )
d01bb5
 
d01bb5
+    def test_group(self):
d01bb5
+        self.assert_find_resources("D", ["D1", "D2"])
d01bb5
+
d01bb5
+    def test_group_in_clone(self):
d01bb5
+        self.assert_find_resources("E", ["E1", "E2"])
d01bb5
+
d01bb5
+    def test_group_in_master(self):
d01bb5
+        self.assert_find_resources("F", ["F1", "F2"])
d01bb5
+
d01bb5
+    def test_cloned_primitive(self):
d01bb5
+        self.assert_find_resources("B-clone", ["B"])
d01bb5
+
d01bb5
+    def test_mastered_primitive(self):
d01bb5
+        self.assert_find_resources("C-master", ["C"])
d01bb5
+
d01bb5
+    def test_bundle_empty(self):
d01bb5
+        self.assert_find_resources("G-bundle", [])
d01bb5
+
d01bb5
+    def test_bundle_with_primitive(self):
d01bb5
+        self.assert_find_resources("H-bundle", ["H"])
d01bb5
+
d01bb5
+
d01bb5
+class FindPrimitives(TestCase, FindResourcesMixin):
d01bb5
+    _tested_fn = staticmethod(common.find_primitives)
d01bb5
+
d01bb5
     def test_primitive(self):
d01bb5
         self.assert_find_resources("A", ["A"])
d01bb5
 
d01bb5
@@ -118,32 +143,155 @@ class FindPrimitives(TestCase):
d01bb5
     def test_primitive_in_bundle(self):
d01bb5
         self.assert_find_resources("H", ["H"])
d01bb5
 
d01bb5
+    def test_cloned_group(self):
d01bb5
+        self.assert_find_resources("E-clone", ["E1", "E2"])
d01bb5
+
d01bb5
+    def test_mastered_group(self):
d01bb5
+        self.assert_find_resources("F-master", ["F1", "F2"])
d01bb5
+
d01bb5
+
d01bb5
+class GetInnerResources(TestCase, FindResourcesMixin):
d01bb5
+    _tested_fn = staticmethod(common.get_inner_resources)
d01bb5
+
d01bb5
+    def test_primitive(self):
d01bb5
+        self.assert_find_resources("A", [])
d01bb5
+
d01bb5
+    def test_primitive_in_clone(self):
d01bb5
+        self.assert_find_resources("B", [])
d01bb5
+
d01bb5
+    def test_primitive_in_master(self):
d01bb5
+        self.assert_find_resources("C", [])
d01bb5
+
d01bb5
+    def test_primitive_in_group(self):
d01bb5
+        self.assert_find_resources("D1", [])
d01bb5
+        self.assert_find_resources("D2", [])
d01bb5
+        self.assert_find_resources("E1", [])
d01bb5
+        self.assert_find_resources("E2", [])
d01bb5
+        self.assert_find_resources("F1", [])
d01bb5
+        self.assert_find_resources("F2", [])
d01bb5
+
d01bb5
+    def test_primitive_in_bundle(self):
d01bb5
+        self.assert_find_resources("H", [])
d01bb5
+
d01bb5
+    def test_mastered_group(self):
d01bb5
+        self.assert_find_resources("F-master", ["F"])
d01bb5
+
d01bb5
+    def test_cloned_group(self):
d01bb5
+        self.assert_find_resources("E-clone", ["E"])
d01bb5
+
d01bb5
+
d01bb5
+class IsWrapperResource(TestCase):
d01bb5
+    def assert_is_wrapper(self, res_id, is_wrapper):
d01bb5
+        self.assertEqual(
d01bb5
+            is_wrapper,
d01bb5
+            common.is_wrapper_resource(
d01bb5
+                fixture_cib.find('.//*[@id="{0}"]'.format(res_id))
d01bb5
+            )
d01bb5
+        )
d01bb5
+
d01bb5
+    def test_primitive(self):
d01bb5
+        self.assert_is_wrapper("A", False)
d01bb5
+
d01bb5
+    def test_primitive_in_clone(self):
d01bb5
+        self.assert_is_wrapper("B", False)
d01bb5
+
d01bb5
+    def test_primitive_in_master(self):
d01bb5
+        self.assert_is_wrapper("C", False)
d01bb5
+
d01bb5
+    def test_primitive_in_group(self):
d01bb5
+        self.assert_is_wrapper("D1", False)
d01bb5
+        self.assert_is_wrapper("D2", False)
d01bb5
+        self.assert_is_wrapper("E1", False)
d01bb5
+        self.assert_is_wrapper("E2", False)
d01bb5
+        self.assert_is_wrapper("F1", False)
d01bb5
+        self.assert_is_wrapper("F2", False)
d01bb5
+
d01bb5
+    def test_primitive_in_bundle(self):
d01bb5
+        self.assert_is_wrapper("H", False)
d01bb5
+
d01bb5
+    def test_cloned_group(self):
d01bb5
+        self.assert_is_wrapper("E-clone", True)
d01bb5
+
d01bb5
+    def test_mastered_group(self):
d01bb5
+        self.assert_is_wrapper("F-master", True)
d01bb5
+
d01bb5
     def test_group(self):
d01bb5
-        self.assert_find_resources("D", ["D1", "D2"])
d01bb5
+        self.assert_is_wrapper("D", True)
d01bb5
 
d01bb5
     def test_group_in_clone(self):
d01bb5
-        self.assert_find_resources("E", ["E1", "E2"])
d01bb5
+        self.assert_is_wrapper("E", True)
d01bb5
 
d01bb5
     def test_group_in_master(self):
d01bb5
-        self.assert_find_resources("F", ["F1", "F2"])
d01bb5
+        self.assert_is_wrapper("F", True)
d01bb5
 
d01bb5
     def test_cloned_primitive(self):
d01bb5
-        self.assert_find_resources("B-clone", ["B"])
d01bb5
-
d01bb5
-    def test_cloned_group(self):
d01bb5
-        self.assert_find_resources("E-clone", ["E1", "E2"])
d01bb5
+        self.assert_is_wrapper("B-clone", True)
d01bb5
 
d01bb5
     def test_mastered_primitive(self):
d01bb5
-        self.assert_find_resources("C-master", ["C"])
d01bb5
+        self.assert_is_wrapper("C-master", True)
d01bb5
+
d01bb5
+    def test_bundle_empty(self):
d01bb5
+        self.assert_is_wrapper("G-bundle", True)
d01bb5
+
d01bb5
+    def test_bundle_with_primitive(self):
d01bb5
+        self.assert_is_wrapper("H-bundle", True)
d01bb5
+
d01bb5
+
d01bb5
+class GetParentResource(TestCase):
d01bb5
+    def assert_parent_resource(self, input_resource_id, output_resource_id):
d01bb5
+        res_el = common.get_parent_resource(fixture_cib.find(
d01bb5
+            './/*[@id="{0}"]'.format(input_resource_id)
d01bb5
+        ))
d01bb5
+        self.assertEqual(
d01bb5
+            output_resource_id, res_el.get("id") if res_el is not None else None
d01bb5
+        )
d01bb5
+
d01bb5
+    def test_primitive(self):
d01bb5
+        self.assert_parent_resource("A", None)
d01bb5
+
d01bb5
+    def test_primitive_in_clone(self):
d01bb5
+        self.assert_parent_resource("B", "B-clone")
d01bb5
+
d01bb5
+    def test_primitive_in_master(self):
d01bb5
+        self.assert_parent_resource("C", "C-master")
d01bb5
+
d01bb5
+    def test_primitive_in_group(self):
d01bb5
+        self.assert_parent_resource("D1", "D")
d01bb5
+        self.assert_parent_resource("D2", "D")
d01bb5
+        self.assert_parent_resource("E1", "E")
d01bb5
+        self.assert_parent_resource("E2", "E")
d01bb5
+        self.assert_parent_resource("F1", "F")
d01bb5
+        self.assert_parent_resource("F2", "F")
d01bb5
+
d01bb5
+    def test_primitive_in_bundle(self):
d01bb5
+        self.assert_parent_resource("H", "H-bundle")
d01bb5
+
d01bb5
+    def test_cloned_group(self):
d01bb5
+        self.assert_parent_resource("E-clone", None)
d01bb5
 
d01bb5
     def test_mastered_group(self):
d01bb5
-        self.assert_find_resources("F-master", ["F1", "F2"])
d01bb5
+        self.assert_parent_resource("F-master", None)
d01bb5
+
d01bb5
+    def test_group(self):
d01bb5
+        self.assert_parent_resource("D", None)
d01bb5
+
d01bb5
+    def test_group_in_clone(self):
d01bb5
+        self.assert_parent_resource("E", "E-clone")
d01bb5
+
d01bb5
+    def test_group_in_master(self):
d01bb5
+        self.assert_parent_resource("F", "F-master")
d01bb5
+
d01bb5
+    def test_cloned_primitive(self):
d01bb5
+        self.assert_parent_resource("B-clone", None)
d01bb5
+
d01bb5
+    def test_mastered_primitive(self):
d01bb5
+        self.assert_parent_resource("C-master", None)
d01bb5
 
d01bb5
     def test_bundle_empty(self):
d01bb5
-        self.assert_find_resources("G-bundle", [])
d01bb5
+        self.assert_parent_resource("G-bundle", None)
d01bb5
 
d01bb5
     def test_bundle_with_primitive(self):
d01bb5
-        self.assert_find_resources("H-bundle", ["H"])
d01bb5
+        self.assert_parent_resource("H-bundle", None)
d01bb5
 
d01bb5
 
d01bb5
 class FindResourcesToEnable(TestCase):
d01bb5
diff --git a/pcs/lib/cib/test/test_resource_relations.py b/pcs/lib/cib/test/test_resource_relations.py
d01bb5
new file mode 100644
d01bb5
index 00000000..63998962
d01bb5
--- /dev/null
d01bb5
+++ b/pcs/lib/cib/test/test_resource_relations.py
d01bb5
@@ -0,0 +1,690 @@
d01bb5
+from lxml import etree
d01bb5
+
d01bb5
+from pcs.test.tools.pcs_unittest import TestCase
d01bb5
+from pcs.common.pacemaker.resource.relations import (
d01bb5
+    RelationEntityDto,
d01bb5
+    ResourceRelationDto,
d01bb5
+    ResourceRelationType,
d01bb5
+)
d01bb5
+from pcs.lib.cib.resource import relations as lib
d01bb5
+
d01bb5
+
d01bb5
+def fixture_cib(resources, constraints):
d01bb5
+    return etree.fromstring("""
d01bb5
+        <cib>
d01bb5
+          <configuration>
d01bb5
+            <resources>
d01bb5
+            {}
d01bb5
+            </resources>
d01bb5
+            <constraints>
d01bb5
+            {}
d01bb5
+            </constraints>
d01bb5
+          </configuration>
d01bb5
+        </cib>
d01bb5
+    """.format(resources, constraints))
d01bb5
+
d01bb5
+
d01bb5
+def fixture_dummy_metadata(_id):
d01bb5
+    return {
d01bb5
+        "id": _id,
d01bb5
+        "class": "c",
d01bb5
+        "provider": "pcmk",
d01bb5
+        "type": "Dummy",
d01bb5
+    }
d01bb5
+
d01bb5
+
d01bb5
+class ResourceRelationNode(TestCase):
d01bb5
+    @staticmethod
d01bb5
+    def entity_fixture(index):
d01bb5
+        return RelationEntityDto.from_dict(dict(
d01bb5
+            id="ent_id{}".format(index),
d01bb5
+            type="ent_type",
d01bb5
+            members=[
d01bb5
+                "{}{}".format(index, member) for member in ("m1", "m2", "m0")
d01bb5
+            ],
d01bb5
+            metadata=dict(
d01bb5
+                id="ent_id{}".format(index),
d01bb5
+                k0="val0",
d01bb5
+                k1="val1",
d01bb5
+            )
d01bb5
+        ))
d01bb5
+
d01bb5
+    def test_no_members(self):
d01bb5
+        ent = self.entity_fixture("0")
d01bb5
+        obj = lib.ResourceRelationNode(ent)
d01bb5
+        self.assertEqual(
d01bb5
+            ResourceRelationDto(ent, [], False).to_dict(),
d01bb5
+            obj.to_dto().to_dict(),
d01bb5
+        )
d01bb5
+
d01bb5
+    def test_with_members(self):
d01bb5
+        ent0 = self.entity_fixture("0")
d01bb5
+        ent1 = self.entity_fixture("1")
d01bb5
+        ent2 = self.entity_fixture("2")
d01bb5
+        ent3 = self.entity_fixture("3")
d01bb5
+        obj = lib.ResourceRelationNode(ent0)
d01bb5
+        obj.add_member(lib.ResourceRelationNode(ent1))
d01bb5
+        member = lib.ResourceRelationNode(ent2)
d01bb5
+        member.add_member(lib.ResourceRelationNode(ent3))
d01bb5
+        obj.add_member(member)
d01bb5
+        self.assertEqual(
d01bb5
+            ResourceRelationDto(
d01bb5
+                ent0,
d01bb5
+                [
d01bb5
+                    ResourceRelationDto(ent1, [], False),
d01bb5
+                    ResourceRelationDto(
d01bb5
+                        ent2,
d01bb5
+                        [
d01bb5
+                            ResourceRelationDto(ent3, [], False),
d01bb5
+                        ],
d01bb5
+                        False,
d01bb5
+                    ),
d01bb5
+                ],
d01bb5
+                False,
d01bb5
+            ).to_dict(),
d01bb5
+            obj.to_dto().to_dict(),
d01bb5
+        )
d01bb5
+
d01bb5
+    def test_stop(self):
d01bb5
+        ent = self.entity_fixture("0")
d01bb5
+        obj = lib.ResourceRelationNode(ent)
d01bb5
+        obj.stop()
d01bb5
+        self.assertEqual(
d01bb5
+            ResourceRelationDto(ent, [], True).to_dict(),
d01bb5
+            obj.to_dto().to_dict(),
d01bb5
+        )
d01bb5
+
d01bb5
+    def test_add_member(self):
d01bb5
+        ent0 = self.entity_fixture("0")
d01bb5
+        ent1 = self.entity_fixture("1")
d01bb5
+        obj = lib.ResourceRelationNode(ent0)
d01bb5
+        obj.add_member(lib.ResourceRelationNode(ent1))
d01bb5
+        self.assertEqual(
d01bb5
+            ResourceRelationDto(
d01bb5
+                ent0, [ResourceRelationDto(ent1, [], False)], False,
d01bb5
+            ).to_dict(),
d01bb5
+            obj.to_dto().to_dict(),
d01bb5
+        )
d01bb5
+
d01bb5
+    def test_add_member_itself(self):
d01bb5
+        ent = self.entity_fixture("0")
d01bb5
+        obj = lib.ResourceRelationNode(ent)
d01bb5
+        obj.add_member(obj)
d01bb5
+        self.assertEqual(
d01bb5
+            ResourceRelationDto(ent, [], False).to_dict(),
d01bb5
+            obj.to_dto().to_dict(),
d01bb5
+        )
d01bb5
+
d01bb5
+    def test_add_member_already_have_parent(self):
d01bb5
+        obj0 = lib.ResourceRelationNode(self.entity_fixture("0"))
d01bb5
+        obj1 = lib.ResourceRelationNode(self.entity_fixture("1"))
d01bb5
+        obj2 = lib.ResourceRelationNode(self.entity_fixture("2"))
d01bb5
+        obj0.add_member(obj1)
d01bb5
+        with self.assertRaises(AssertionError):
d01bb5
+            obj2.add_member(obj1)
d01bb5
+
d01bb5
+    def test_add_member_already_in_branch(self):
d01bb5
+        ent0 = self.entity_fixture("0")
d01bb5
+        ent1 = self.entity_fixture("1")
d01bb5
+        obj0 = lib.ResourceRelationNode(ent0)
d01bb5
+        obj1 = lib.ResourceRelationNode(ent1)
d01bb5
+        obj0.add_member(obj1)
d01bb5
+        obj1.add_member(obj0)
d01bb5
+        self.assertEqual(
d01bb5
+            ResourceRelationDto(
d01bb5
+                ent0, [ResourceRelationDto(ent1, [], False)], False
d01bb5
+            ).to_dict(),
d01bb5
+            obj0.to_dto().to_dict(),
d01bb5
+        )
d01bb5
+
d01bb5
+    def test_add_member_already_in_different_branch(self):
d01bb5
+        ent0 = self.entity_fixture("0")
d01bb5
+        ent1 = self.entity_fixture("1")
d01bb5
+        obj0 = lib.ResourceRelationNode(ent0)
d01bb5
+        obj0.add_member(lib.ResourceRelationNode(ent1))
d01bb5
+        obj0.add_member(lib.ResourceRelationNode(ent1))
d01bb5
+        self.assertEqual(
d01bb5
+            ResourceRelationDto(
d01bb5
+                ent0, [
d01bb5
+                    ResourceRelationDto(ent1, [], False),
d01bb5
+                    ResourceRelationDto(ent1, [], False),
d01bb5
+                ],
d01bb5
+                False,
d01bb5
+            ).to_dict(),
d01bb5
+            obj0.to_dto().to_dict(),
d01bb5
+        )
d01bb5
+
d01bb5
+
d01bb5
+class ResourceRelationsFetcher(TestCase):
d01bb5
+    def test_ordering_constraint(self):
d01bb5
+        obj = lib.ResourceRelationsFetcher(fixture_cib(
d01bb5
+            """
d01bb5
+            <primitive id="d1" class="c" provider="pcmk" type="Dummy"/>
d01bb5
+            <primitive id="d2" class="c" provider="pcmk" type="Dummy"/>
d01bb5
+            """,
d01bb5
+            """
d01bb5
+            
d01bb5
+                id="order-d1-d2-mandatory" then="d2" then-action="start"
d01bb5
+                kind="Mandatory"/>
d01bb5
+            """
d01bb5
+        ))
d01bb5
+        expected = (
d01bb5
+            {
d01bb5
+                "d1": RelationEntityDto(
d01bb5
+                    "d1",
d01bb5
+                    "primitive",
d01bb5
+                    ["order-d1-d2-mandatory"],
d01bb5
+                    fixture_dummy_metadata("d1"),
d01bb5
+                ),
d01bb5
+                "d2": RelationEntityDto(
d01bb5
+                    "d2",
d01bb5
+                    "primitive",
d01bb5
+                    ["order-d1-d2-mandatory"],
d01bb5
+                    fixture_dummy_metadata("d2"),
d01bb5
+                ),
d01bb5
+            },
d01bb5
+            {
d01bb5
+                "order-d1-d2-mandatory": RelationEntityDto(
d01bb5
+                    "order-d1-d2-mandatory",
d01bb5
+                    ResourceRelationType.ORDER,
d01bb5
+                    members=["d1", "d2"],
d01bb5
+                    metadata={
d01bb5
+                        "id": "order-d1-d2-mandatory",
d01bb5
+                        "first": "d1",
d01bb5
+                        "first-action": "start",
d01bb5
+                        "then": "d2",
d01bb5
+                        "then-action": "start",
d01bb5
+                        "kind": "Mandatory",
d01bb5
+                    },
d01bb5
+                ),
d01bb5
+            },
d01bb5
+        )
d01bb5
+        for res in ("d1", "d2"):
d01bb5
+            self.assertEqual(expected, obj.get_relations(res))
d01bb5
+
d01bb5
+    def test_ordering_set_constraint(self):
d01bb5
+        obj = lib.ResourceRelationsFetcher(fixture_cib(
d01bb5
+            """
d01bb5
+            <primitive id="d1" class="c" provider="pcmk" type="Dummy"/>
d01bb5
+            <primitive id="d2" class="c" provider="pcmk" type="Dummy"/>
d01bb5
+            <primitive id="d3" class="c" provider="pcmk" type="Dummy"/>
d01bb5
+            <primitive id="d4" class="c" provider="pcmk" type="Dummy"/>
d01bb5
+            <primitive id="d5" class="c" provider="pcmk" type="Dummy"/>
d01bb5
+            <primitive id="d6" class="c" provider="pcmk" type="Dummy"/>
d01bb5
+            """,
d01bb5
+            """
d01bb5
+            
d01bb5
+                id="pcs_rsc_order_set_1">
d01bb5
+              
d01bb5
+                  id="pcs_rsc_set_1">
d01bb5
+                <resource_ref id="d1"/>
d01bb5
+                <resource_ref id="d3"/>
d01bb5
+                <resource_ref id="d2"/>
d01bb5
+              </resource_set>
d01bb5
+              
d01bb5
+                  require-all="false" id="pcs_rsc_set_2">
d01bb5
+                <resource_ref id="d6"/>
d01bb5
+                <resource_ref id="d5"/>
d01bb5
+                <resource_ref id="d4"/>
d01bb5
+              </resource_set>
d01bb5
+            </rsc_order>
d01bb5
+            """
d01bb5
+        ))
d01bb5
+        rsc_entity = lambda _id: RelationEntityDto(
d01bb5
+            _id,
d01bb5
+            "primitive",
d01bb5
+            ["pcs_rsc_order_set_1"],
d01bb5
+            fixture_dummy_metadata(_id),
d01bb5
+        )
d01bb5
+        res_list = ("d1", "d2", "d3", "d4", "d5", "d6")
d01bb5
+        expected = (
d01bb5
+            {_id: rsc_entity(_id) for _id in res_list},
d01bb5
+            {
d01bb5
+                "pcs_rsc_order_set_1": RelationEntityDto(
d01bb5
+                    "pcs_rsc_order_set_1",
d01bb5
+                    ResourceRelationType.ORDER_SET,
d01bb5
+                    members=["d1", "d2", "d3", "d4", "d5", "d6"],
d01bb5
+                    metadata={
d01bb5
+                        "id": "pcs_rsc_order_set_1",
d01bb5
+                        "sets": [
d01bb5
+                            {
d01bb5
+                                "id": "pcs_rsc_set_1",
d01bb5
+                                "metadata": {
d01bb5
+                                    "id": "pcs_rsc_set_1",
d01bb5
+                                    "sequential": "true",
d01bb5
+                                    "require-all": "true",
d01bb5
+                                    "action": "start",
d01bb5
+                                },
d01bb5
+                                "members": ["d1", "d3", "d2"],
d01bb5
+                            },
d01bb5
+                            {
d01bb5
+                                "id": "pcs_rsc_set_2",
d01bb5
+                                "metadata": {
d01bb5
+                                    "id": "pcs_rsc_set_2",
d01bb5
+                                    "sequential": "false",
d01bb5
+                                    "require-all": "false",
d01bb5
+                                    "action": "stop",
d01bb5
+                                },
d01bb5
+                                "members": ["d6", "d5", "d4"],
d01bb5
+                            },
d01bb5
+                        ],
d01bb5
+                        "kind": "Serialize",
d01bb5
+                        "symmetrical": "true",
d01bb5
+                    },
d01bb5
+                ),
d01bb5
+            },
d01bb5
+        )
d01bb5
+        for res in res_list:
d01bb5
+            self.assertEqual(expected, obj.get_relations(res))
d01bb5
+
d01bb5
+    def test_group(self):
d01bb5
+        obj = lib.ResourceRelationsFetcher(fixture_cib(
d01bb5
+            """
d01bb5
+            <group id="g1">
d01bb5
+              <primitive id="d1" class="c" provider="pcmk" type="Dummy"/>
d01bb5
+              <primitive id="d2" class="c" provider="pcmk" type="Dummy"/>
d01bb5
+            </group>
d01bb5
+            """,
d01bb5
+            ""
d01bb5
+        ))
d01bb5
+        expected = (
d01bb5
+            {
d01bb5
+                "d1": RelationEntityDto(
d01bb5
+                    "d1",
d01bb5
+                    "primitive",
d01bb5
+                    ["outer:g1"],
d01bb5
+                    fixture_dummy_metadata("d1"),
d01bb5
+                ),
d01bb5
+                "d2": RelationEntityDto(
d01bb5
+                    "d2",
d01bb5
+                    "primitive",
d01bb5
+                    ["outer:g1"],
d01bb5
+                    fixture_dummy_metadata("d2"),
d01bb5
+                ),
d01bb5
+                "g1": RelationEntityDto(
d01bb5
+                    "g1", "group", ["inner:g1"], {"id": "g1"}
d01bb5
+                ),
d01bb5
+            },
d01bb5
+            {
d01bb5
+                "inner:g1": RelationEntityDto(
d01bb5
+                    "inner:g1",
d01bb5
+                    ResourceRelationType.INNER_RESOURCES,
d01bb5
+                    ["d1", "d2"],
d01bb5
+                    {"id": "g1"},
d01bb5
+                ),
d01bb5
+                "outer:g1": RelationEntityDto(
d01bb5
+                    "outer:g1",
d01bb5
+                    ResourceRelationType.OUTER_RESOURCE,
d01bb5
+                    ["g1"],
d01bb5
+                    {"id": "g1"},
d01bb5
+                ),
d01bb5
+            },
d01bb5
+        )
d01bb5
+        for res in ("d1", "d2", "g1"):
d01bb5
+            self.assertEqual(expected, obj.get_relations(res))
d01bb5
+
d01bb5
+    def _test_wrapper(self, wrapper_tag):
d01bb5
+        obj = lib.ResourceRelationsFetcher(fixture_cib(
d01bb5
+            """
d01bb5
+            <{0} id="w1">
d01bb5
+              <primitive id="d1" class="c" provider="pcmk" type="Dummy"/>
d01bb5
+            </{0}>
d01bb5
+            """.format(wrapper_tag),
d01bb5
+            ""
d01bb5
+        ))
d01bb5
+        expected = (
d01bb5
+            {
d01bb5
+                "d1": RelationEntityDto(
d01bb5
+                    "d1",
d01bb5
+                    "primitive",
d01bb5
+                    ["outer:w1"],
d01bb5
+                    fixture_dummy_metadata("d1"),
d01bb5
+                ),
d01bb5
+                "w1": RelationEntityDto(
d01bb5
+                    "w1", wrapper_tag, ["inner:w1"], {"id": "w1"}
d01bb5
+                ),
d01bb5
+            },
d01bb5
+            {
d01bb5
+                "inner:w1": RelationEntityDto(
d01bb5
+                    "inner:w1",
d01bb5
+                    ResourceRelationType.INNER_RESOURCES,
d01bb5
+                    ["d1"],
d01bb5
+                    {"id": "w1"},
d01bb5
+                ),
d01bb5
+                "outer:w1": RelationEntityDto(
d01bb5
+                    "outer:w1",
d01bb5
+                    ResourceRelationType.OUTER_RESOURCE,
d01bb5
+                    ["w1"],
d01bb5
+                    {"id": "w1"},
d01bb5
+                ),
d01bb5
+            },
d01bb5
+        )
d01bb5
+        for res in ("d1", "w1"):
d01bb5
+            self.assertEqual(expected, obj.get_relations(res))
d01bb5
+
d01bb5
+    def test_clone(self):
d01bb5
+        self._test_wrapper("clone")
d01bb5
+
d01bb5
+    def test_bundle(self):
d01bb5
+        self._test_wrapper("bundle")
d01bb5
+
d01bb5
+    def test_cloned_group(self):
d01bb5
+        obj = lib.ResourceRelationsFetcher(fixture_cib(
d01bb5
+            """
d01bb5
+            <clone id="c1">
d01bb5
+                <group id="g1">
d01bb5
+                  <primitive id="d1" class="c" provider="pcmk" type="Dummy"/>
d01bb5
+                  <primitive id="d2" class="c" provider="pcmk" type="Dummy"/>
d01bb5
+                </group>
d01bb5
+            </clone>
d01bb5
+            """,
d01bb5
+            ""
d01bb5
+        ))
d01bb5
+        expected = (
d01bb5
+            {
d01bb5
+                "d1": RelationEntityDto(
d01bb5
+                    "d1",
d01bb5
+                    "primitive",
d01bb5
+                    ["outer:g1"],
d01bb5
+                    fixture_dummy_metadata("d1"),
d01bb5
+                ),
d01bb5
+                "d2": RelationEntityDto(
d01bb5
+                    "d2",
d01bb5
+                    "primitive",
d01bb5
+                    ["outer:g1"],
d01bb5
+                    fixture_dummy_metadata("d2"),
d01bb5
+                ),
d01bb5
+                "g1": RelationEntityDto(
d01bb5
+                    "g1", "group", ["outer:c1", "inner:g1"], {"id": "g1"}
d01bb5
+                ),
d01bb5
+                "c1": RelationEntityDto(
d01bb5
+                    "c1", "clone", ["inner:c1"], {"id": "c1"}
d01bb5
+                )
d01bb5
+            },
d01bb5
+            {
d01bb5
+                "inner:g1": RelationEntityDto(
d01bb5
+                    "inner:g1",
d01bb5
+                    ResourceRelationType.INNER_RESOURCES,
d01bb5
+                    ["d1", "d2"],
d01bb5
+                    {"id": "g1"},
d01bb5
+                ),
d01bb5
+                "outer:g1": RelationEntityDto(
d01bb5
+                    "outer:g1",
d01bb5
+                    ResourceRelationType.OUTER_RESOURCE,
d01bb5
+                    ["g1"],
d01bb5
+                    {"id": "g1"},
d01bb5
+                ),
d01bb5
+                "inner:c1": RelationEntityDto(
d01bb5
+                    "inner:c1",
d01bb5
+                    ResourceRelationType.INNER_RESOURCES,
d01bb5
+                    ["g1"],
d01bb5
+                    {"id": "c1"}
d01bb5
+                ),
d01bb5
+                "outer:c1": RelationEntityDto(
d01bb5
+                    "outer:c1",
d01bb5
+                    ResourceRelationType.OUTER_RESOURCE,
d01bb5
+                    ["c1"],
d01bb5
+                    {"id": "c1"}
d01bb5
+                ),
d01bb5
+            },
d01bb5
+        )
d01bb5
+        for res in ("d1", "d2", "g1", "c1"):
d01bb5
+            self.assertEqual(expected, obj.get_relations(res))
d01bb5
+
d01bb5
+
d01bb5
+class ResourceRelationTreeBuilder(TestCase):
d01bb5
+    @staticmethod
d01bb5
+    def primitive_fixture(_id, members):
d01bb5
+        return RelationEntityDto(
d01bb5
+            _id, "primitive", members, fixture_dummy_metadata(_id)
d01bb5
+        )
d01bb5
+
d01bb5
+    def test_resource_not_present(self):
d01bb5
+        with self.assertRaises(AssertionError):
d01bb5
+            lib.ResourceRelationTreeBuilder({}, {}).get_tree("not_existing")
d01bb5
+
d01bb5
+    def test_simple_order(self):
d01bb5
+        resources_members = ["order-d1-d2-mandatory"]
d01bb5
+        resources = {
d01bb5
+            "d1": self.primitive_fixture("d1", resources_members),
d01bb5
+            "d2": self.primitive_fixture("d2", resources_members),
d01bb5
+        }
d01bb5
+        relations = {
d01bb5
+            "order-d1-d2-mandatory": RelationEntityDto(
d01bb5
+                "order-d1-d2-mandatory",
d01bb5
+                ResourceRelationType.ORDER,
d01bb5
+                members=["d1", "d2"],
d01bb5
+                metadata={
d01bb5
+                    "id": "order-d1-d2-mandatory",
d01bb5
+                    "first": "d1",
d01bb5
+                    "first-action": "start",
d01bb5
+                    "then": "d2",
d01bb5
+                    "then-action": "start",
d01bb5
+                    "kind": "Mandatory",
d01bb5
+                },
d01bb5
+            ),
d01bb5
+        }
d01bb5
+        expected = dict(
d01bb5
+            relation_entity=resources["d2"].to_dict(),
d01bb5
+            is_leaf=False,
d01bb5
+            members=[
d01bb5
+                dict(
d01bb5
+                    relation_entity=(
d01bb5
+                        relations["order-d1-d2-mandatory"].to_dict()
d01bb5
+                    ),
d01bb5
+                    is_leaf=False,
d01bb5
+                    members=[
d01bb5
+                        dict(
d01bb5
+                            relation_entity=resources["d1"].to_dict(),
d01bb5
+                            is_leaf=False,
d01bb5
+                            members=[],
d01bb5
+                        )
d01bb5
+                    ]
d01bb5
+                )
d01bb5
+            ],
d01bb5
+        )
d01bb5
+        self.assertEqual(
d01bb5
+            expected,
d01bb5
+            lib.ResourceRelationTreeBuilder(
d01bb5
+                resources, relations
d01bb5
+            ).get_tree("d2").to_dto().to_dict()
d01bb5
+        )
d01bb5
+
d01bb5
+    def test_simple_order_set(self):
d01bb5
+        res_list = ("d1", "d2", "d3", "d4", "d5", "d6")
d01bb5
+        resources_members = ["pcs_rsc_order_set_1"]
d01bb5
+        resources = {
d01bb5
+            _id: self.primitive_fixture(_id, resources_members)
d01bb5
+            for _id in res_list
d01bb5
+        }
d01bb5
+        relations = {
d01bb5
+            "pcs_rsc_order_set_1": RelationEntityDto(
d01bb5
+                "pcs_rsc_order_set_1",
d01bb5
+                ResourceRelationType.ORDER_SET,
d01bb5
+                members=["d1", "d2", "d3", "d4", "d5", "d6"],
d01bb5
+                metadata={
d01bb5
+                    "id": "pcs_rsc_order_set_1",
d01bb5
+                    "sets": [
d01bb5
+                        {
d01bb5
+                            "id": "pcs_rsc_set_1",
d01bb5
+                            "metadata": {
d01bb5
+                                "id": "pcs_rsc_set_1",
d01bb5
+                                "sequential": "true",
d01bb5
+                                "require-all": "true",
d01bb5
+                                "action": "start",
d01bb5
+                            },
d01bb5
+                            "members": ["d1", "d3", "d2"],
d01bb5
+                        },
d01bb5
+                        {
d01bb5
+                            "id": "pcs_rsc_set_2",
d01bb5
+                            "metadata": {
d01bb5
+                                "id": "pcs_rsc_set_2",
d01bb5
+                                "sequential": "false",
d01bb5
+                                "require-all": "false",
d01bb5
+                                "action": "stop",
d01bb5
+                            },
d01bb5
+                            "members": ["d6", "d5", "d4"],
d01bb5
+                        },
d01bb5
+                    ],
d01bb5
+                    "kind": "Serialize",
d01bb5
+                    "symmetrical": "true",
d01bb5
+                },
d01bb5
+            ),
d01bb5
+        }
d01bb5
+        get_res = lambda _id: dict(
d01bb5
+            relation_entity=resources[_id].to_dict(),
d01bb5
+            is_leaf=False,
d01bb5
+            members=[],
d01bb5
+        )
d01bb5
+        expected = dict(
d01bb5
+            relation_entity=resources["d5"].to_dict(),
d01bb5
+            is_leaf=False,
d01bb5
+            members=[
d01bb5
+                dict(
d01bb5
+                    relation_entity=relations["pcs_rsc_order_set_1"].to_dict(),
d01bb5
+                    is_leaf=False,
d01bb5
+                    members=[
d01bb5
+                        get_res(_id) for _id in ("d1", "d2", "d3", "d4", "d6")
d01bb5
+                    ]
d01bb5
+                )
d01bb5
+            ],
d01bb5
+        )
d01bb5
+        self.assertEqual(
d01bb5
+            expected,
d01bb5
+            lib.ResourceRelationTreeBuilder(
d01bb5
+                resources, relations
d01bb5
+            ).get_tree("d5").to_dto().to_dict()
d01bb5
+        )
d01bb5
+
d01bb5
+    def test_simple_in_group(self):
d01bb5
+        resources_members = ["outer:g1"]
d01bb5
+        resources = {
d01bb5
+            "d1": self.primitive_fixture("d1", resources_members),
d01bb5
+            "d2": self.primitive_fixture("d2", resources_members),
d01bb5
+            "g1": RelationEntityDto(
d01bb5
+                "g1", "group", ["inner:g1"], {"id": "g1"}
d01bb5
+            ),
d01bb5
+        }
d01bb5
+        relations = {
d01bb5
+            "inner:g1": RelationEntityDto(
d01bb5
+                "inner:g1",
d01bb5
+                ResourceRelationType.INNER_RESOURCES,
d01bb5
+                ["d1", "d2"],
d01bb5
+                {"id": "g1"},
d01bb5
+            ),
d01bb5
+            "outer:g1": RelationEntityDto(
d01bb5
+                "outer:g1",
d01bb5
+                ResourceRelationType.OUTER_RESOURCE,
d01bb5
+                ["g1"],
d01bb5
+                {"id": "g1"},
d01bb5
+            ),
d01bb5
+        }
d01bb5
+        expected = dict(
d01bb5
+            relation_entity=resources["d1"].to_dict(),
d01bb5
+            is_leaf=False,
d01bb5
+            members=[
d01bb5
+                dict(
d01bb5
+                    relation_entity=relations["outer:g1"].to_dict(),
d01bb5
+                    is_leaf=False,
d01bb5
+                    members=[
d01bb5
+                        dict(
d01bb5
+                            relation_entity=resources["g1"].to_dict(),
d01bb5
+                            is_leaf=False,
d01bb5
+                            members=[
d01bb5
+                                dict(
d01bb5
+                                    relation_entity=(
d01bb5
+                                        relations["inner:g1"].to_dict()
d01bb5
+                                    ),
d01bb5
+                                    is_leaf=False,
d01bb5
+                                    members=[
d01bb5
+                                        dict(
d01bb5
+                                            relation_entity=(
d01bb5
+                                                resources["d2"].to_dict()
d01bb5
+                                            ),
d01bb5
+                                            is_leaf=False,
d01bb5
+                                            members=[],
d01bb5
+                                        ),
d01bb5
+                                    ],
d01bb5
+                                ),
d01bb5
+                            ],
d01bb5
+                        ),
d01bb5
+                    ],
d01bb5
+                ),
d01bb5
+            ],
d01bb5
+        )
d01bb5
+        self.assertEqual(
d01bb5
+            expected,
d01bb5
+            lib.ResourceRelationTreeBuilder(
d01bb5
+                resources, relations
d01bb5
+            ).get_tree("d1").to_dto().to_dict()
d01bb5
+        )
d01bb5
+
d01bb5
+    def test_order_loop(self):
d01bb5
+        resources_members = ["order-d1-d2-mandatory", "order-d2-d1-mandatory"]
d01bb5
+        resources = {
d01bb5
+            "d1": self.primitive_fixture("d1", resources_members),
d01bb5
+            "d2": self.primitive_fixture("d2", resources_members),
d01bb5
+        }
d01bb5
+        order_fixture = lambda r1, r2: RelationEntityDto(
d01bb5
+            "order-{}-{}-mandatory".format(r1, r2),
d01bb5
+            ResourceRelationType.ORDER,
d01bb5
+            members=[r1, r2],
d01bb5
+            metadata={
d01bb5
+                "id": "order-{}-{}-mandatory".format(r1, r2),
d01bb5
+                "first": r1,
d01bb5
+                "first-action": "start",
d01bb5
+                "then": r2,
d01bb5
+                "then-action": "start",
d01bb5
+                "kind": "Mandatory",
d01bb5
+            },
d01bb5
+        )
d01bb5
+        relations = {
d01bb5
+            "order-d1-d2-mandatory": order_fixture("d1", "d2"),
d01bb5
+            "order-d2-d1-mandatory": order_fixture("d2", "d1"),
d01bb5
+        }
d01bb5
+        expected = dict(
d01bb5
+            relation_entity=resources["d1"].to_dict(),
d01bb5
+            is_leaf=False,
d01bb5
+            members=[
d01bb5
+                dict(
d01bb5
+                    relation_entity=(
d01bb5
+                        relations["order-d1-d2-mandatory"].to_dict()
d01bb5
+                    ),
d01bb5
+                    is_leaf=False,
d01bb5
+                    members=[
d01bb5
+                        dict(
d01bb5
+                            relation_entity=resources["d2"].to_dict(),
d01bb5
+                            is_leaf=False,
d01bb5
+                            members=[
d01bb5
+                                dict(
d01bb5
+                                    relation_entity=relations[
d01bb5
+                                        "order-d2-d1-mandatory"
d01bb5
+                                    ].to_dict(),
d01bb5
+                                    is_leaf=True,
d01bb5
+                                    members=[],
d01bb5
+                                ),
d01bb5
+                            ],
d01bb5
+                        ),
d01bb5
+                    ],
d01bb5
+                ),
d01bb5
+                dict(
d01bb5
+                    relation_entity=(
d01bb5
+                        relations["order-d2-d1-mandatory"].to_dict()
d01bb5
+                    ),
d01bb5
+                    is_leaf=False,
d01bb5
+                    members=[
d01bb5
+                        dict(
d01bb5
+                            relation_entity=resources["d2"].to_dict(),
d01bb5
+                            is_leaf=True,
d01bb5
+                            members=[],
d01bb5
+                        ),
d01bb5
+                    ],
d01bb5
+                ),
d01bb5
+            ],
d01bb5
+        )
d01bb5
+        self.assertEqual(
d01bb5
+            expected,
d01bb5
+            lib.ResourceRelationTreeBuilder(
d01bb5
+                resources, relations
d01bb5
+            ).get_tree("d1").to_dto().to_dict()
d01bb5
+        )
d01bb5
diff --git a/pcs/lib/commands/resource.py b/pcs/lib/commands/resource.py
d01bb5
index 0073964d..09a68a68 100644
d01bb5
--- a/pcs/lib/commands/resource.py
d01bb5
+++ b/pcs/lib/commands/resource.py
d01bb5
@@ -897,6 +897,28 @@ def get_failcounts(
d01bb5
         interval=interval_ms
d01bb5
     )
d01bb5
 
d01bb5
+
d01bb5
+def get_resource_relations_tree(env, resource_id):
d01bb5
+    """
d01bb5
+    Return a dict representing tree-like structure of resources and their
d01bb5
+    relations.
d01bb5
+
d01bb5
+    env -- library environment
d01bb5
+    resource_id -- id of a resource which should be the root of the relation
d01bb5
+        tree
d01bb5
+    """
d01bb5
+    cib = env.get_cib()
d01bb5
+    _find_resources_or_raise(get_resources(cib), [resource_id])
d01bb5
+    resources_dict, relations_dict = (
d01bb5
+        resource.relations.ResourceRelationsFetcher(
d01bb5
+            cib
d01bb5
+        ).get_relations(resource_id)
d01bb5
+    )
d01bb5
+    return resource.relations.ResourceRelationTreeBuilder(
d01bb5
+        resources_dict, relations_dict
d01bb5
+    ).get_tree(resource_id).to_dto().to_dict()
d01bb5
+
d01bb5
+
d01bb5
 def _find_resources_or_raise(
d01bb5
     resources_section, resource_ids, additional_search=None
d01bb5
 ):
d01bb5
diff --git a/pcs/lib/commands/test/resource/test_resource_relations.py b/pcs/lib/commands/test/resource/test_resource_relations.py
d01bb5
new file mode 100644
d01bb5
index 00000000..404780f3
d01bb5
--- /dev/null
d01bb5
+++ b/pcs/lib/commands/test/resource/test_resource_relations.py
d01bb5
@@ -0,0 +1,300 @@
d01bb5
+from pcs.test.tools.pcs_unittest import TestCase
d01bb5
+from pcs.test.tools import fixture
d01bb5
+from pcs.test.tools.command_env import get_env_tools
d01bb5
+
d01bb5
+from pcs.common.pacemaker.resource.relations import ResourceRelationType
d01bb5
+from pcs.lib.commands import resource
d01bb5
+
d01bb5
+
d01bb5
+def fixture_primitive(_id, members):
d01bb5
+    return dict(
d01bb5
+        id=_id,
d01bb5
+        type="primitive",
d01bb5
+        metadata={
d01bb5
+            "id": _id,
d01bb5
+            "class": "ocf",
d01bb5
+            "provider": "pacemaker",
d01bb5
+            "type": "Dummy",
d01bb5
+        },
d01bb5
+        members=members,
d01bb5
+    )
d01bb5
+
d01bb5
+
d01bb5
+def fixture_primitive_xml(_id):
d01bb5
+    return """
d01bb5
+        <primitive id="{}" class="ocf" provider="pacemaker" type="Dummy"/>
d01bb5
+    """.format(_id)
d01bb5
+
d01bb5
+def fixture_node(entity, members=None, leaf=False):
d01bb5
+    return dict(
d01bb5
+        relation_entity=entity,
d01bb5
+        is_leaf=leaf,
d01bb5
+        members=members or [],
d01bb5
+    )
d01bb5
+
d01bb5
+def fixture_order(res1, res2, kind="Mandatory", score=None):
d01bb5
+    _id = "order-{}-{}".format(res1, res2)
d01bb5
+    out = dict(
d01bb5
+        id=_id,
d01bb5
+        type=ResourceRelationType.ORDER,
d01bb5
+        members=[res1, res2],
d01bb5
+        metadata={
d01bb5
+            "id": _id,
d01bb5
+            "first": res1,
d01bb5
+            "first-action": "start",
d01bb5
+            "then": res2,
d01bb5
+            "then-action": "start",
d01bb5
+            "kind": kind,
d01bb5
+        },
d01bb5
+    )
d01bb5
+    if score:
d01bb5
+        out["metadata"]["score"] = score
d01bb5
+    return out
d01bb5
+
d01bb5
+
d01bb5
+class GetResourceRelationsTree(TestCase):
d01bb5
+    def setUp(self):
d01bb5
+        self.env_assist, self.config = get_env_tools(test_case=self)
d01bb5
+
d01bb5
+    def test_not_existing_resource(self):
d01bb5
+        self.config.runner.cib.load()
d01bb5
+        resource_id = "not_existing"
d01bb5
+        self.env_assist.assert_raise_library_error(
d01bb5
+            lambda: resource.get_resource_relations_tree(
d01bb5
+                self.env_assist.get_env(),
d01bb5
+                resource_id,
d01bb5
+            ),
d01bb5
+            [
d01bb5
+                fixture.report_not_found(resource_id, context_type="resources"),
d01bb5
+            ],
d01bb5
+            expected_in_processor=False,
d01bb5
+        )
d01bb5
+
d01bb5
+    def test_simple(self):
d01bb5
+        self.config.runner.cib.load(
d01bb5
+            resources="<resources>{}</resources>".format(
d01bb5
+                fixture_primitive_xml("d1") + fixture_primitive_xml("d2")
d01bb5
+            ),
d01bb5
+            constraints="""
d01bb5
+            <constraints>
d01bb5
+                
d01bb5
+                    id="order-d1-d2" then="d2" then-action="start"
d01bb5
+                    kind="Mandatory"/>
d01bb5
+            </constraints>
d01bb5
+            """,
d01bb5
+        )
d01bb5
+        prim_members = ["order-d1-d2"]
d01bb5
+        expected = fixture_node(
d01bb5
+            fixture_primitive("d1", prim_members),
d01bb5
+            [
d01bb5
+                fixture_node(
d01bb5
+                    fixture_order("d1", "d2"),
d01bb5
+                    [fixture_node(fixture_primitive("d2", prim_members))],
d01bb5
+                )
d01bb5
+            ],
d01bb5
+        )
d01bb5
+        self.assertEqual(
d01bb5
+            expected,
d01bb5
+            resource.get_resource_relations_tree(
d01bb5
+                self.env_assist.get_env(), "d1"
d01bb5
+            )
d01bb5
+        )
d01bb5
+
d01bb5
+class GetResourceRelationsTreeComplex(TestCase):
d01bb5
+    def setUp(self):
d01bb5
+        self.env_assist, self.config = get_env_tools(test_case=self)
d01bb5
+        self.config.runner.cib.load(
d01bb5
+            resources="""
d01bb5
+                <resources>
d01bb5
+                    {primitives}
d01bb5
+                    <clone id="c">
d01bb5
+                        <group id="cg">
d01bb5
+                        {in_group}
d01bb5
+                        </group>
d01bb5
+                    </clone>
d01bb5
+                </resources>
d01bb5
+            """.format(
d01bb5
+                primitives=(
d01bb5
+                    fixture_primitive_xml("d1") +
d01bb5
+                    fixture_primitive_xml("d2") +
d01bb5
+                    fixture_primitive_xml("d3")
d01bb5
+                ),
d01bb5
+                in_group=(
d01bb5
+                    fixture_primitive_xml("cgd1") +
d01bb5
+                    fixture_primitive_xml("cgd2") +
d01bb5
+                    fixture_primitive_xml("cgd0")
d01bb5
+                )
d01bb5
+            ),
d01bb5
+            constraints="""
d01bb5
+            <constraints>
d01bb5
+              
d01bb5
+                  then="d2" then-action="start" kind="Mandatory"/>
d01bb5
+              
d01bb5
+                  then="d2" then-action="start" kind="Optional" score="10"/>
d01bb5
+              
d01bb5
+                  id="pcs_rsc_order_set_1">
d01bb5
+                
d01bb5
+                    action="start" id="pcs_rsc_set_1">
d01bb5
+                  <resource_ref id="d1"/>
d01bb5
+                  <resource_ref id="d3"/>
d01bb5
+                </resource_set>
d01bb5
+                
d01bb5
+                    require-all="false" id="pcs_rsc_set_2">
d01bb5
+                  <resource_ref id="cg"/>
d01bb5
+                  <resource_ref id="d2"/>
d01bb5
+                </resource_set>
d01bb5
+              </rsc_order>
d01bb5
+            </constraints>
d01bb5
+            """,
d01bb5
+        )
d01bb5
+        self.d1_members = ["order-d1-d2", "pcs_rsc_order_set_1"]
d01bb5
+        self.d2_members = [
d01bb5
+            "order-d1-d2", "pcs_rsc_order_set_1", "order-cgd1-d2",
d01bb5
+        ]
d01bb5
+        self.order_set = dict(
d01bb5
+            id="pcs_rsc_order_set_1",
d01bb5
+            type=ResourceRelationType.ORDER_SET,
d01bb5
+            members=["cg", "d1", "d2", "d3"],
d01bb5
+            metadata={
d01bb5
+                "id": "pcs_rsc_order_set_1",
d01bb5
+                "sets": [
d01bb5
+                    {
d01bb5
+                        "id": "pcs_rsc_set_1",
d01bb5
+                        "metadata": {
d01bb5
+                            "id": "pcs_rsc_set_1",
d01bb5
+                            "sequential": "true",
d01bb5
+                            "require-all": "true",
d01bb5
+                            "action": "start",
d01bb5
+                        },
d01bb5
+                        "members": ["d1", "d3"],
d01bb5
+                    },
d01bb5
+                    {
d01bb5
+                        "id": "pcs_rsc_set_2",
d01bb5
+                        "metadata": {
d01bb5
+                            "id": "pcs_rsc_set_2",
d01bb5
+                            "sequential": "false",
d01bb5
+                            "require-all": "false",
d01bb5
+                            "action": "stop",
d01bb5
+                        },
d01bb5
+                        "members": ["cg", "d2"],
d01bb5
+                    },
d01bb5
+                ],
d01bb5
+                "kind": "Serialize",
d01bb5
+                "symmetrical": "true",
d01bb5
+            },
d01bb5
+        )
d01bb5
+        self.cg_ent = dict(
d01bb5
+            id="cg",
d01bb5
+            type="group",
d01bb5
+            members=["inner:cg", "pcs_rsc_order_set_1", "outer:c"],
d01bb5
+            metadata=dict(id="cg"),
d01bb5
+        )
d01bb5
+
d01bb5
+    def test_d1(self):
d01bb5
+        outer_cg = dict(
d01bb5
+            id="outer:cg",
d01bb5
+            type=ResourceRelationType.OUTER_RESOURCE,
d01bb5
+            members=["cg"],
d01bb5
+            metadata=dict(id="cg"),
d01bb5
+        )
d01bb5
+        order_opt = fixture_order("cgd1", "d2", kind="Optional", score="10")
d01bb5
+        expected = fixture_node(
d01bb5
+            fixture_primitive("d1", self.d1_members), [
d01bb5
+                fixture_node(
d01bb5
+                    fixture_order("d1", "d2"), [
d01bb5
+                        fixture_node(
d01bb5
+                            fixture_primitive("d2", self.d2_members), [
d01bb5
+                                fixture_node(self.order_set, leaf=True),
d01bb5
+                                fixture_node(
d01bb5
+                                    order_opt, [
d01bb5
+                                        fixture_node(
d01bb5
+                                            fixture_primitive(
d01bb5
+                                                "cgd1",
d01bb5
+                                                ["order-cgd1-d2", "outer:cg"],
d01bb5
+                                            ), [
d01bb5
+                                                fixture_node(
d01bb5
+                                                    outer_cg, [
d01bb5
+                                                        fixture_node(
d01bb5
+                                                            self.cg_ent,
d01bb5
+                                                            leaf=True,
d01bb5
+                                                        ),
d01bb5
+                                                    ]
d01bb5
+                                                )
d01bb5
+                                            ]
d01bb5
+                                        ),
d01bb5
+                                    ],
d01bb5
+                                ),
d01bb5
+                            ],
d01bb5
+                        )
d01bb5
+                    ]
d01bb5
+                ),
d01bb5
+                fixture_node(
d01bb5
+                    self.order_set, [
d01bb5
+                        fixture_node(
d01bb5
+                            self.cg_ent, [
d01bb5
+                                fixture_node(
d01bb5
+                                    dict(
d01bb5
+                                        id="inner:cg",
d01bb5
+                                        type=(
d01bb5
+                                            ResourceRelationType.INNER_RESOURCES
d01bb5
+                                        ),
d01bb5
+                                        members=["cgd1", "cgd2", "cgd0"],
d01bb5
+                                        metadata=dict(id="cg"),
d01bb5
+                                    ), [
d01bb5
+                                        fixture_node(
d01bb5
+                                            fixture_primitive(
d01bb5
+                                                "cgd1",
d01bb5
+                                                ["order-cgd1-d2", "outer:cg"],
d01bb5
+                                            ),
d01bb5
+                                            leaf=True,
d01bb5
+                                        ),
d01bb5
+                                        fixture_node(
d01bb5
+                                            fixture_primitive(
d01bb5
+                                                "cgd2", ["outer:cg"]
d01bb5
+                                            ),
d01bb5
+                                        ),
d01bb5
+                                        fixture_node(
d01bb5
+                                            fixture_primitive(
d01bb5
+                                                "cgd0", ["outer:cg"]
d01bb5
+                                            ),
d01bb5
+                                        ),
d01bb5
+                                    ]
d01bb5
+                                ),
d01bb5
+                                fixture_node(
d01bb5
+                                    dict(
d01bb5
+                                        id="outer:c",
d01bb5
+                                        type=(
d01bb5
+                                            ResourceRelationType.OUTER_RESOURCE
d01bb5
+                                        ),
d01bb5
+                                        members=["c"],
d01bb5
+                                        metadata=dict(id="c")
d01bb5
+                                    ), [
d01bb5
+                                        fixture_node(
d01bb5
+                                            dict(
d01bb5
+                                                id="c",
d01bb5
+                                                type="clone",
d01bb5
+                                                members=["inner:c"],
d01bb5
+                                                metadata=dict(id="c"),
d01bb5
+                                            ),
d01bb5
+                                            []
d01bb5
+                                        )
d01bb5
+                                    ]
d01bb5
+                                )
d01bb5
+                            ],
d01bb5
+                        ),
d01bb5
+                        fixture_node(
d01bb5
+                            fixture_primitive("d2", self.d2_members), leaf=True
d01bb5
+                        ),
d01bb5
+                        fixture_node(
d01bb5
+                            fixture_primitive("d3", ["pcs_rsc_order_set_1"]),
d01bb5
+                        )
d01bb5
+                    ],
d01bb5
+                )
d01bb5
+            ],
d01bb5
+        )
d01bb5
+        self.assertEqual(
d01bb5
+            expected,
d01bb5
+            resource.get_resource_relations_tree(
d01bb5
+                self.env_assist.get_env(), "d1"
d01bb5
+            )
d01bb5
+        )
d01bb5
diff --git a/pcs/pcs.8 b/pcs/pcs.8
d01bb5
index f08b5e46..0e8b15f7 100644
d01bb5
--- a/pcs/pcs.8
d01bb5
+++ b/pcs/pcs.8
d01bb5
@@ -209,6 +209,9 @@ Remove all constraints created by the 'relocate run' command.
d01bb5
 .TP
d01bb5
 utilization [<resource id> [<name>=<value> ...]]
d01bb5
 Add specified utilization options to specified resource. If resource is not specified, shows utilization of all resources. If utilization options are not specified, shows utilization of specified resource. Utilization option should be in format name=value, value has to be integer. Options may be removed by setting an option without a value. Example: pcs resource utilization TestResource cpu= ram=20
d01bb5
+.TP
d01bb5
+relations <resource id> [\fB\-\-full\fR]
d01bb5
+Display relations of a resource specified by its id with other resources in a tree structure. Supported types of resource relations are: ordering constraints, ordering set constraints, relations defined by resource hierarchy (clones, groups, bundles). If \fB\-\-full\fR is used, more verbose output will be printed.
d01bb5
 .SS "cluster"
d01bb5
 .TP
d01bb5
 auth [<node>[:<port>]] [...] [\fB\-u\fR <username>] [\fB\-p\fR <password>] [\fB\-\-force\fR] [\fB\-\-local\fR]
d01bb5
diff --git a/pcs/resource.py b/pcs/resource.py
d01bb5
index d3761327..27af2405 100644
d01bb5
--- a/pcs/resource.py
d01bb5
+++ b/pcs/resource.py
d01bb5
@@ -28,6 +28,7 @@ from pcs.cli.resource.parse_args import (
d01bb5
     parse_bundle_update_options,
d01bb5
     parse_create as parse_create_args,
d01bb5
 )
d01bb5
+from pcs.cli.resource.relations import show_resource_relations_cmd
d01bb5
 import pcs.lib.cib.acl as lib_acl
d01bb5
 from pcs.lib.cib.resource import (
d01bb5
     guest_node,
d01bb5
@@ -200,6 +201,8 @@ def resource_cmd(argv):
d01bb5
             get_resource_agent_info(argv_next)
d01bb5
         elif sub_cmd == "bundle":
d01bb5
             resource_bundle_cmd(lib, argv_next, modifiers)
d01bb5
+        elif sub_cmd == "relations":
d01bb5
+            show_resource_relations_cmd(lib, argv_next, modifiers)
d01bb5
         else:
d01bb5
             usage.resource()
d01bb5
             sys.exit(1)
d01bb5
diff --git a/pcs/test/tools/fixture_cib.py b/pcs/test/tools/fixture_cib.py
d01bb5
index 907650a7..04be4d09 100644
d01bb5
--- a/pcs/test/tools/fixture_cib.py
d01bb5
+++ b/pcs/test/tools/fixture_cib.py
d01bb5
@@ -132,6 +132,9 @@ MODIFIER_GENERATORS = {
d01bb5
     "append": append_all,
d01bb5
     "resources": lambda xml: replace_all({"./configuration/resources": xml}),
d01bb5
     "optional_in_conf": lambda xml: put_or_replace("./configuration", xml),
d01bb5
+    "constraints": lambda xml: replace_all(
d01bb5
+        {"./configuration/constraints": xml}
d01bb5
+    ),
d01bb5
     #common modifier `put_or_replace` makes not sense - see explanation inside
d01bb5
     #this function - all occurences should be satisfied by `optional_in_conf`
d01bb5
 }
d01bb5
diff --git a/pcs/usage.py b/pcs/usage.py
d01bb5
index 37c92d26..582bc53f 100644
d01bb5
--- a/pcs/usage.py
d01bb5
+++ b/pcs/usage.py
d01bb5
@@ -552,6 +552,13 @@ Commands:
d01bb5
         integer. Options may be removed by setting an option without a value.
d01bb5
         Example: pcs resource utilization TestResource cpu= ram=20
d01bb5
 
d01bb5
+    relations <resource id> [--full]
d01bb5
+        Display relations of a resource specified by its id with other resources
d01bb5
+        in a tree structure. Supported types of resource relations are:
d01bb5
+        ordering constraints, ordering set constraints, relations defined by
d01bb5
+        resource hierarchy (clones, groups, bundles). If --full is used, more
d01bb5
+        verbose output will be printed.
d01bb5
+
d01bb5
 Examples:
d01bb5
 
d01bb5
     pcs resource show
d01bb5
diff --git a/pcsd/capabilities.xml b/pcsd/capabilities.xml
d01bb5
index 80fcd33b..bc1d69c8 100644
d01bb5
--- a/pcsd/capabilities.xml
d01bb5
+++ b/pcsd/capabilities.xml
d01bb5
@@ -1261,6 +1261,14 @@
d01bb5
         pcs commands: resource restart
d01bb5
       </description>
d01bb5
     </capability>
d01bb5
+    <capability id="pcmk.resource.relations" in-pcs="1" in-pcsd="0">
d01bb5
+      <description>
d01bb5
+        Display relations of a resource specified by its id with other resources
d01bb5
+        in a tree structure.
d01bb5
+
d01bb5
+        pcs commands: resource relations
d01bb5
+      </description>
d01bb5
+    </capability>
d01bb5
 
d01bb5
 
d01bb5
 
d01bb5
diff --git a/test/centos7/Dockerfile b/test/centos7/Dockerfile
d01bb5
index 2969f8d5..9313dd1f 100644
d01bb5
--- a/test/centos7/Dockerfile
d01bb5
+++ b/test/centos7/Dockerfile
d01bb5
@@ -10,7 +10,7 @@ RUN yum install -y \
d01bb5
         python \
d01bb5
         python-devel \
d01bb5
         python-lxml \
d01bb5
-        python-mock \
d01bb5
+        python-pip \
d01bb5
         python-pycurl \
d01bb5
         # ruby
d01bb5
         ruby \
d01bb5
@@ -40,6 +40,8 @@ RUN yum install -y \
d01bb5
         fence-virt \
d01bb5
         booth-site
d01bb5
 
d01bb5
+RUN pip install mock
d01bb5
+
d01bb5
 COPY . $src_path
d01bb5
 
d01bb5
 # build ruby gems
d01bb5
-- 
d01bb5
2.20.1
d01bb5