Blob Blame History Raw
From 5fa38022bbfe6134f71622174a0961d0e0bc3138 Mon Sep 17 00:00:00 2001
From: Ondrej Mular <omular@redhat.com>
Date: Thu, 14 Nov 2019 13:40:57 +0100
Subject: [PATCH 1/6] add `resource relations` command

This new commad displays relations of a specified resource
---
 pcs/cli/common/lib_wrapper.py                 |   2 +
 pcs/cli/common/printable_tree.py              |  50 ++
 pcs/cli/common/test/test_printable_tree.py    | 216 ++++++
 pcs/cli/resource/relations.py                 | 202 +++++
 pcs/cli/resource/test/test_relations.py       | 492 +++++++++++++
 pcs/common/interface/__init__.py              |   0
 pcs/common/interface/dto.py                   |  18 +
 pcs/common/pacemaker/__init__.py              |   0
 pcs/common/pacemaker/resource/__init__.py     |   0
 pcs/common/pacemaker/resource/relations.py    |  66 ++
 .../pacemaker/resource/test/__init__.py       |   0
 .../pacemaker/resource/test/test_relations.py | 169 +++++
 pcs/lib/cib/resource/__init__.py              |   1 +
 pcs/lib/cib/resource/common.py                |  48 ++
 pcs/lib/cib/resource/relations.py             | 265 +++++++
 pcs/lib/cib/test/test_resource_common.py      | 174 ++++-
 pcs/lib/cib/test/test_resource_relations.py   | 690 ++++++++++++++++++
 pcs/lib/commands/resource.py                  |  22 +
 .../test/resource/test_resource_relations.py  | 300 ++++++++
 pcs/pcs.8                                     |   3 +
 pcs/resource.py                               |   3 +
 pcs/test/tools/fixture_cib.py                 |   3 +
 pcs/usage.py                                  |   7 +
 pcsd/capabilities.xml                         |   8 +
 test/centos7/Dockerfile                       |   4 +-
 25 files changed, 2729 insertions(+), 14 deletions(-)
 create mode 100644 pcs/cli/common/printable_tree.py
 create mode 100644 pcs/cli/common/test/test_printable_tree.py
 create mode 100644 pcs/cli/resource/relations.py
 create mode 100644 pcs/cli/resource/test/test_relations.py
 create mode 100644 pcs/common/interface/__init__.py
 create mode 100644 pcs/common/interface/dto.py
 create mode 100644 pcs/common/pacemaker/__init__.py
 create mode 100644 pcs/common/pacemaker/resource/__init__.py
 create mode 100644 pcs/common/pacemaker/resource/relations.py
 create mode 100644 pcs/common/pacemaker/resource/test/__init__.py
 create mode 100644 pcs/common/pacemaker/resource/test/test_relations.py
 create mode 100644 pcs/lib/cib/resource/relations.py
 create mode 100644 pcs/lib/cib/test/test_resource_relations.py
 create mode 100644 pcs/lib/commands/test/resource/test_resource_relations.py

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