diff --git a/README-topology.md b/README-topology.md
new file mode 100644
index 0000000000000000000000000000000000000000..5294a89a1cb9cdef453298d6d2d6dd911095427f
--- /dev/null
+++ b/README-topology.md
@@ -0,0 +1,154 @@
+Topology modules
+================
+
+Description
+-----------
+
+These modules allow to manage the topology. That means that topology segments can be added, removed and reinitialized. Also it is possible to verify topology suffixes.
+
+
+Features
+--------
+* Topology management
+
+
+Supported FreeIPA Versions
+--------------------------
+
+FreeIPA versions 4.4.0 and up are supported by the ipatopologysegment and ipatopologysuffix modules.
+
+
+Requirements
+------------
+
+**Controller**
+* Ansible version: 2.8+
+
+**Node**
+* Supported FreeIPA version (see above)
+
+
+Usage
+=====
+
+Example inventory file
+
+```ini
+[ipaserver]
+ipaserver.test.local
+```
+
+
+Example playbook to add a topology segment wiht default name (cn):
+
+```yaml
+---
+- name: Playbook to handle topologysegment
+  hosts: ipaserver
+  become: true
+
+  tasks:
+  - name: Add topology segment
+    ipatopologysegment:
+      password: MyPassword123
+      suffix: domain
+      left: ipareplica1.test.local
+      right: ipareplica2.test.local
+      state: present
+```
+The name (cn) can also be set if it should not be the default `{left}-to-{rkight}`.
+
+
+Example playbook to delete a topology segment:
+
+```yaml
+---
+- name: Playbook to handle topologysegment
+  hosts: ipaserver
+  become: true
+
+  tasks:
+  - name: Delete topology segment
+    ipatopologysegment:
+      password: MyPassword123
+      suffix: domain
+      left: ipareplica1.test.local
+      right: ipareplica2.test.local
+      state: absent
+```
+It is possible to either use the name (cn) or left and right nodes. If left and right nodes are used, then the name will be searched and used internally.
+
+
+Example playbook to reinitialize a topology segment:
+
+```yaml
+---
+- name: Playbook to handle topologysegment
+  hosts: ipaserver
+  become: true
+
+  tasks:
+  - name: Reinitialize topology segment
+    ipatopologysegment:
+      password: MyPassword123
+      suffix: domain
+      left: ipareplica1.test.local
+      right: ipareplica2.test.local
+      direction: left-to-right
+      state: reinitialized
+```
+It is possible to either use the name (cn) or left and right nodes. If left and right nodes are used, then the name will be searched and used internally.
+
+
+Example playbook to verify a topology suffix:
+
+```yaml
+---
+- name: Playbook to handle topologysuffix
+  hosts: ipaserver
+  become: true
+
+  tasks:
+  - name: Verify topology suffix
+    ipatopologysuffix:
+      password: MyPassword123
+      suffix: domain
+      state: verified
+```
+
+
+Variables
+=========
+
+ipatopologysegment
+------------------
+
+Variable | Description | Required
+-------- | ----------- | --------
+`principal` | The admin principal is a string and defaults to `admin` | no
+`password` | The admin password is a string and is required if there is no admin ticket available on the node | no
+`suffix` | The topology suffix to be used, this can either be `domain` or `ca` | yes
+`name` \| `cn` | The topology segment name (cn) is the unique identifier for a segment. | no
+`left` \| `leftnode` | The left replication node string - an IPA server | no
+`right` \| `rightnode` | The right replication node string - an IPA server | no
+`direction` | The direction a segment will be reinitialized. It can either be `left-to-right` or `right-to-left` and only used with `state: reinitialized` | 
+`state` | The state to ensure. It can be one of `present`, `absent`, `enabled`, `disabled` or `reinitialized` | yes
+
+
+ipatopologysuffix
+-----------------
+
+Verify FreeIPA topology suffix
+
+Variable | Description | Required
+-------- | ----------- | --------
+`principal` | The admin principal is a string and defaults to `admin` | no
+`password` | The admin password is a string and is required if there is no admin ticket available on the node | no
+`suffix` | The topology suffix to be used, this can either be `domain` or `ca` | yes
+`state` | The state to ensure. It can only be `verified` | yes
+
+
+Authors
+=======
+
+Thomas Woerner
diff --git a/README.md b/README.md
index 81dc3f11291634449c019ab4743e98a07039c1fc..6e3d2853c87a118b12a28f092e01cd02dfa58659 100644
--- a/README.md
+++ b/README.md
@@ -11,6 +11,7 @@ Features
 * Cluster deployments: Server, replicas and clients in one playbook
 * One-time-password (OTP) support for client installation
 * Repair mode for clients
+* Modules for topology management
 
 Supported FreeIPA Versions
 --------------------------
@@ -307,3 +308,9 @@ Roles
 * [Server](roles/ipaserver/README.md)
 * [Replica](roles/ipareplica/README.md)
 * [Client](roles/ipaclient/README.md)
+
+Plugins in plugin/modules
+=========================
+
+* [ipatopologysegment](README-topology.md)
+* [ipatopologysuffix](README-topology.md)
diff --git a/playbooks/topology/add-topologysegment.yml b/playbooks/topology/add-topologysegment.yml
new file mode 100644
index 0000000000000000000000000000000000000000..a6c8477d673a15dd30265cfcd8b06a1e18c864c8
--- /dev/null
+++ b/playbooks/topology/add-topologysegment.yml
@@ -0,0 +1,13 @@
+---
+- name: Playbook to handle topologysegment
+  hosts: ipaserver
+  become: true
+
+  tasks:
+  - name: Add topology segment
+    ipatopologysegment:
+      password: MyPassword123
+      suffix: domain
+      left: ipareplica1.test.local
+      right: ipareplica2.test.local
+      state: present
diff --git a/playbooks/topology/delete-topologysegment.yml b/playbooks/topology/delete-topologysegment.yml
new file mode 100644
index 0000000000000000000000000000000000000000..af640137961b90caaf729d621e6290acd61a0a65
--- /dev/null
+++ b/playbooks/topology/delete-topologysegment.yml
@@ -0,0 +1,13 @@
+---
+- name: Playbook to handle topologysegment
+  hosts: ipaserver
+  become: true
+
+  tasks:
+  - name: Delete topology segment
+    ipatopologysegment:
+      password: MyPassword123
+      suffix: domain
+      left: ipareplica1.test.local
+      right: ipareplica2.test.local
+      state: absent
diff --git a/playbooks/topology/reinitialize-topologysegment.yml b/playbooks/topology/reinitialize-topologysegment.yml
new file mode 100644
index 0000000000000000000000000000000000000000..7afdd65a28b31cf1b8b94012b2db450f6680e331
--- /dev/null
+++ b/playbooks/topology/reinitialize-topologysegment.yml
@@ -0,0 +1,14 @@
+---
+- name: Playbook to handle topologysegment
+  hosts: ipaserver
+  become: true
+
+  tasks:
+  - name: Reinitialize topology segment
+    ipatopologysegment:
+      password: MyPassword123
+      suffix: domain
+      left: ipareplica1.test.local
+      right: ipareplica2.test.local
+      direction: left-to-right
+      state: reinitialized
diff --git a/playbooks/topology/verify-topologysuffix.yml b/playbooks/topology/verify-topologysuffix.yml
new file mode 100644
index 0000000000000000000000000000000000000000..518fc7c219196e3e39b31efdad64d9d2272d487e
--- /dev/null
+++ b/playbooks/topology/verify-topologysuffix.yml
@@ -0,0 +1,11 @@
+---
+- name: Playbook to handle topologysuffix
+  hosts: ipaserver
+  become: true
+
+  tasks:
+  - name: Verify topology suffix
+    ipatopologysuffix:
+      password: MyPassword123
+      suffix: domain
+      state: verified
diff --git a/plugins/module_utils/ansible_freeipa_module.py b/plugins/module_utils/ansible_freeipa_module.py
new file mode 100644
index 0000000000000000000000000000000000000000..421ee68fdb42e1ffaa5528826ec366e7cdd6c765
--- /dev/null
+++ b/plugins/module_utils/ansible_freeipa_module.py
@@ -0,0 +1,122 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Authors:
+#   Thomas Woerner <twoerner@redhat.com>
+#
+# Copyright (C) 2019  Red Hat
+# see file 'COPYING' for use and warranty information
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+
+import os
+import sys
+import tempfile
+import shutil
+from ipalib import api, errors
+from ipalib.config import Env
+from ipalib.constants import DEFAULT_CONFIG
+try:
+    from ipalib.install.kinit import kinit_password
+except ImportError:
+    from ipapython.ipautil import kinit_password
+from ipapython.ipautil import run
+from ipaplatform.paths import paths
+from ipalib.krb_utils import get_credentials_if_valid
+
+
+def valid_creds(principal):
+    """
+    Get valid credintials matching the princial
+    """
+    creds = get_credentials_if_valid()
+    if creds and \
+       creds.lifetime > 0 and \
+       "%s@" % principal in creds.name.display_as(creds.name.name_type):
+        return True
+    return False
+
+
+def temp_kinit(principal, password):
+    """
+    kinit with password using a temporary ccache
+    """
+    if not password:
+        raise RuntimeError("The password is not set")
+    if not principal:
+        principal = "admin"
+
+    ccache_dir = tempfile.mkdtemp(prefix='krbcc')
+    ccache_name = os.path.join(ccache_dir, 'ccache')
+
+    try:
+        kinit_password(principal, password, ccache_name)
+    except RuntimeError as e:
+        raise RuntimeError("Kerberos authentication failed: {}".format(e))
+
+    return ccache_dir, ccache_name
+
+
+def temp_kdestroy(ccache_dir, ccache_name):
+    """
+    Destroy temporary ticket and remove temporary ccache
+    """
+    if ccache_name is not None:
+        run([paths.KDESTROY, '-c', ccache_name], raiseonerr=False)
+    if ccache_dir is not None:
+        shutil.rmtree(ccache_dir, ignore_errors=True)
+
+
+def api_connect():
+    """
+    Create environment, initialize api and connect to ldap2
+    """
+    env = Env()
+    env._bootstrap()
+    env._finalize_core(**dict(DEFAULT_CONFIG))
+
+    api.bootstrap(context='server', debug=env.debug, log=None)
+    api.finalize()
+    api.Backend.ldap2.connect()
+
+
+def api_command(module, command, name, args):
+    """
+    Call ipa.Command, use AnsibleModule.fail_json for error handling
+    """
+    try:
+        return api.Command[command](name, **args)
+    except Exception as e:
+        module.fail_json(msg="%s: %s" % (command, e))
+
+
+def execute_api_command(module, principal, password, command, name, args):
+    """
+    Get KRB ticket if not already there, initialize api, connect,
+    execute command and destroy ticket again if it has been created also.
+    """
+    ccache_dir = None
+    ccache_name = None
+    try:
+        if not valid_creds(principal):
+            ccache_dir, ccache_name = temp_kinit(principal, password)
+        api_connect()
+
+        return api_command(module, command, name, args)
+    except Exception as e:
+        module.fail_json(msg=str(e))
+
+    finally:
+        temp_kdestroy(ccache_dir, ccache_name)
diff --git a/plugins/modules/ipatopologysegment.py b/plugins/modules/ipatopologysegment.py
new file mode 100644
index 0000000000000000000000000000000000000000..20a5d090605f0829cdceb128f6105b6d0f94ecbb
--- /dev/null
+++ b/plugins/modules/ipatopologysegment.py
@@ -0,0 +1,288 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Authors:
+#   Thomas Woerner <twoerner@redhat.com>
+#
+# Copyright (C) 2019 Red Hat
+# see file 'COPYING' for use and warranty information
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+ANSIBLE_METADATA = {
+    "metadata_version": "1.0",
+    "supported_by": "community",
+    "status": ["preview"],
+}
+
+DOCUMENTATION = """
+---
+module: ipatopologysegment
+short description: Manage FreeIPA topology segments
+description: Manage FreeIPA topology segments
+options:
+  principal:
+    description: The admin principal
+    default: admin
+  password:
+    description: The admin password
+    required: false
+  suffix:
+    description: Topology suffix
+    required: true
+    choices: ["domain", "ca"]
+  name:
+    description: Topology segment name, unique identifier.
+    required: false
+    aliases: ["cn"]
+  left:
+    description: Left replication node - an IPA server
+    aliases: ["leftnode"]
+  right:
+    description: Right replication node - an IPA server
+    aliases: ["rightnode"]
+  direction:
+    description: The direction a segment will be reinitialized
+    required: false
+    choices: ["left-to-right", "right-to-left"]
+  state:
+    description: State to ensure
+    default: present
+    choices: ["present", "absent", "enabled", "disabled", "reinitialized"]
+author:
+    - Thomas Woerner
+"""
+
+EXAMPLES = """
+- ipatopologysegment:
+    suffix: domain
+    left: ipaserver.test.local
+    right: ipareplica1.test.local
+    state: present
+
+- ipatopologysegment:
+    suffix: domain
+    name: ipaserver.test.local-to-replica1.test.local
+    state: absent
+
+- ipatopologysegment:
+    suffix: domain
+    left: ipaserver.test.local
+    right: ipareplica1.test.local
+    state: absent
+
+- ipatopologysegment:
+    suffix: ca
+    name: ipaserver.test.local-to-replica1.test.local
+    direction: left-to-right
+    state: reinitialized
+"""
+
+RETURN = """
+"""
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible.module_utils._text import to_bytes, to_native, to_text
+from ansible.module_utils.ansible_freeipa_module import temp_kinit, \
+    temp_kdestroy, valid_creds, api_connect, api_command
+
+def find_left_right(module, suffix, left, right):
+    _args = {
+        "iparepltoposegmentleftnode": to_text(left),
+        "iparepltoposegmentrightnode": to_text(right),
+    }
+    _result = api_command(module, "topologysegment_find",
+                          to_text(suffix), _args)
+    if len(_result["result"]) > 1:
+        module.fail_json(
+            msg="Combination of left node '%s' and right node '%s' is "
+            "not unique for suffix '%s'" % (left, right, suffix))
+    elif len(_result["result"]) == 1:
+        return _result["result"][0]
+    else:
+        return None
+
+
+def find_cn(module, suffix, name):
+    _args = {
+        "cn": to_text(name),
+    }
+    _result = api_command(module, "topologysegment_find",
+                          to_text(suffix), _args)
+    if len(_result["result"]) > 1:
+        module.fail_json(
+            msg="CN '%s' is not unique for suffix '%s'" % (name, suffix))
+    elif len(_result["result"]) == 1:
+        return _result["result"][0]
+    else:
+        return None
+
+
+def main():
+    ansible_module = AnsibleModule(
+        argument_spec=dict(
+            principal=dict(type="str", default="admin"),
+            password=dict(type="str", required=False, no_log=True),
+            suffix=dict(choices=["domain", "ca"], required=True),
+            name=dict(type="str", aliases=["cn"], default=None),
+            left=dict(type="str", aliases=["leftnode"], default=None),
+            right=dict(type="str", aliases=["rightnode"], default=None),
+            direction=dict(type="str", default=None,
+                           choices=["left-to-right", "right-to-left"]),
+            state=dict(type="str", default="present",
+                       choices=["present", "absent", "enabled", "disabled",
+                                "reinitialized"]),
+        ),
+        supports_check_mode=True,
+    )
+
+    ansible_module._ansible_debug = True
+
+    # Get parameters
+
+    principal = ansible_module.params.get("principal")
+    password = ansible_module.params.get("password")
+    suffix = ansible_module.params.get("suffix")
+    name = ansible_module.params.get("name")
+    left = ansible_module.params.get("left")
+    right = ansible_module.params.get("right")
+    direction = ansible_module.params.get("direction")
+    state = ansible_module.params.get("state")
+
+    # Check parameters
+
+    if state != "reinitialized" and direction is not None:
+        ansible_module.fail_json(
+            msg="Direction is not supported in this mode.")
+
+    # Init
+
+    changed = False
+    ccache_dir = None
+    ccache_name = None
+    try:
+        if not valid_creds(principal):
+            ccache_dir, ccache_name = temp_kinit(principal, password)
+        api_connect()
+
+        command = None
+
+        # Get name (cn) from left and right node if set for absent, disabled
+        # or reinitialized.
+        if state in ["absent", "disabled", "reinitialized"]:
+            if left is not None and right is not None:
+                left_right = find_left_right(ansible_module, suffix,
+                                             left, right)
+                if left_right is not None:
+                    if name is not None and \
+                       left_right["cn"][0] != to_text(name):
+                        ansible_module.fail_json(
+                            msg="Left and right nodes do not match "
+                            "given name name (cn) '%s'" % name)
+                    args = {
+                        "cn": left_right["cn"][0]
+                    }
+                # else: Nothing to change
+            elif name is not None:
+                result = find_cn(ansible_module, suffix, name)
+                if result is not None:
+                    args = {
+                        "cn": result["cn"][0]
+                    }
+                # else: Nothing to change
+            else:
+                ansible_module.fail_json(
+                    msg="Either left and right or name need to be set.")
+
+        # Create command
+        if state in ["present", "enabled"]:
+            # Make sure topology segment exists
+
+            if left is None or right is None:
+                ansible_module.fail_json(
+                    msg="Left and right need to be set.")
+            args = {
+                "iparepltoposegmentleftnode": to_text(left),
+                "iparepltoposegmentrightnode": to_text(right),
+            }
+            if name is not None:
+                args["cn"] = to_text(name)
+
+            res_left_right = find_left_right(ansible_module, suffix,
+                                             left, right)
+            if res_left_right is not None:
+                if name is not None and \
+                   res_left_right["cn"][0] != to_text(name):
+                    ansible_module.fail_json(
+                        msg="Left and right nodes already used with "
+                        "different name (cn) '%s'" % res_left_right["cn"])
+
+                # Left and right nodes and also the name can not be
+                # changed
+                for key in [ "iparepltoposegmentleftnode",
+                             "iparepltoposegmentrightnode" ]:
+                    if key in args:
+                        del args[key]
+                if len(args) > 1:
+                    # cn needs to be in args always
+                    command = "topologysegment_mod"
+                # else: Nothing to change
+            else:
+                if name is None:
+                    args["cn"] = to_text("%s-to-%s" % (left, right))
+                command = "topologysegment_add"
+
+        elif state in ["absent", "disabled"]:
+            # Make sure topology segment does not exist
+
+            if len(args) > 0:
+                # Either name defined or found name from left and right node
+                command = "topologysegment_del"
+
+        elif state == "reinitialized":
+            # Reinitialize segment
+
+            if len(args) > 0:
+                # Either name defined or found name from left and right node
+                command = "topologysegment_reinitialize"
+
+                if direction == "left-to-right":
+                    args["left"] = True
+                elif direction == "right-to-left":
+                    args["right"] = True
+                else:
+                    ansible_module.fail_json(msg="Unknown direction '%s'" %
+                                             direction)
+        else:
+            ansible_module.fail_json(msg="Unkown state '%s'" % state)
+
+        # Execute command
+
+        if command is not None:
+            result = api_command(ansible_module, command,
+                                 to_text(suffix), args)
+            changed = True
+
+    except Exception as e:
+        ansible_module.fail_json(msg=str(e))
+
+    finally:
+        temp_kdestroy(ccache_dir, ccache_name)
+
+    # Done
+
+    ansible_module.exit_json(changed=changed)
+
+if __name__ == "__main__":
+    main()
diff --git a/plugins/modules/ipatopologysuffix.py b/plugins/modules/ipatopologysuffix.py
new file mode 100644
index 0000000000000000000000000000000000000000..a71adc2c21707961a17624c4821b2208ac26c46e
--- /dev/null
+++ b/plugins/modules/ipatopologysuffix.py
@@ -0,0 +1,109 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Authors:
+#   Thomas Woerner <twoerner@redhat.com>
+#
+# Copyright (C) 2019 Red Hat
+# see file 'COPYING' for use and warranty information
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+ANSIBLE_METADATA = {
+    "metadata_version": "1.0",
+    "supported_by": "community",
+    "status": ["preview"],
+}
+
+DOCUMENTATION = """
+---
+module: ipatopologysuffix
+short description: Verify FreeIPA topology suffix
+description: Verify FreeIPA topology suffix
+options:
+  principal:
+    description: The admin principal
+    default: admin
+  password:
+    description: The admin password
+    required: false
+  suffix:
+    description: Topology suffix
+    required: true
+    choices: ["domain", "ca"]
+  state:
+    description: State to ensure
+    default: verified
+    choices: ["verified"]
+author:
+    - Thomas Woerner
+"""
+
+EXAMPLES = """
+- ipatopologysuffix:
+    suffix: domain
+    state: verified
+"""
+
+RETURN = """
+"""
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible.module_utils._text import to_bytes, to_native, to_text
+from ansible.module_utils.ansible_freeipa_module import execute_api_command
+
+def main():
+    ansible_module = AnsibleModule(
+        argument_spec=dict(
+            principal=dict(type="str", default="admin"),
+            password=dict(type="str", required=False, no_log=True),
+            suffix=dict(choices=["domain", "ca"], required=True),
+            state=dict(type="str", default="verified",
+                       choices=["verified"]),
+        ),
+        supports_check_mode=True,
+    )
+
+    ansible_module._ansible_debug = True
+
+    # Get parameters
+
+    principal = ansible_module.params.get("principal")
+    password = ansible_module.params.get("password")
+    suffix = ansible_module.params.get("suffix")
+    state = ansible_module.params.get("state")
+
+    # Check parameters
+
+    # Init
+
+    # Create command
+
+    if state in ["verified"]:
+        command = "topologysuffix_verify"
+        args = {}
+    else:
+        ansible_module.fail_json(msg="Unkown state '%s'" % state)
+
+    # Execute command
+
+    execute_api_command(ansible_module, principal, password,
+                        command, to_text(suffix), args)
+
+    # Done
+
+    ansible_module.exit_json(changed=True)
+
+if __name__ == "__main__":
+    main()