diff --git a/README-server.md b/README-server.md
new file mode 100644
index 0000000000000000000000000000000000000000..b899cdaca02141e60b55018deb6717600c987f7b
--- /dev/null
+++ b/README-server.md
@@ -0,0 +1,248 @@
+Server module
+============
+
+Description
+-----------
+
+The server module allows to ensure presence and absence of servers. The module requires an existing server, the deployment of a new server can not be done with the module.
+
+Features
+--------
+
+* Server management
+
+
+Supported FreeIPA Versions
+--------------------------
+
+FreeIPA versions 4.4.0 and up are supported by the ipaserver module.
+
+
+Requirements
+------------
+
+**Controller**
+* Ansible version: 2.8+
+
+**Node**
+* Supported FreeIPA version (see above)
+
+
+Usage
+=====
+
+Example inventory file
+
+```ini
+[ipaserver]
+ipaserver.test.local
+```
+
+
+Example playbook to make sure server "server.example.com" is present:
+
+```yaml
+---
+- name: Playbook to manage IPA server.
+  hosts: ipaserver
+  become: yes
+
+  tasks:
+  - ipaserver:
+      ipaadmin_password: SomeADMINpassword
+      name: server.example.com
+```
+
+
+Example playbook to make sure server "server.example.com" is present with location mylocation:
+
+```yaml
+---
+- name: Playbook to manage IPA server.
+  hosts: ipaserver
+  become: yes
+
+  tasks:
+  - ipaserver:
+      ipaadmin_password: SomeADMINpassword
+      name: server.example.com
+      location: mylocation
+```
+
+
+Example playbook to make sure server "server.example.com" is present without a location:
+
+```yaml
+---
+- name: Playbook to manage IPA server.
+  hosts: ipaserver
+  become: yes
+
+  tasks:
+  - ipaserver:
+      ipaadmin_password: SomeADMINpassword
+      name: server.example.com
+      location: ""
+```
+
+
+Example playbook to make sure server "server.example.com" is present with service weight 1:
+
+```yaml
+---
+- name: Playbook to manage IPA server.
+  hosts: ipaserver
+  become: yes
+
+  tasks:
+  - ipaserver:
+      ipaadmin_password: SomeADMINpassword
+      name: server.example.com
+      service_weight: 1
+```
+
+
+Example playbook to make sure server "server.example.com" is present without service weight:
+
+```yaml
+---
+- name: Playbook to manage IPA server.
+  hosts: ipaserver
+  become: yes
+
+  tasks:
+  - ipaserver:
+      ipaadmin_password: SomeADMINpassword
+      name: server.example.com
+      service_weight: -1
+```
+
+
+Example playbook to make sure server "server.example.com" is present and hidden:
+
+```yaml
+---
+- name: Playbook to manage IPA server.
+  hosts: ipaserver
+  become: yes
+
+  tasks:
+  - ipaserver:
+      ipaadmin_password: SomeADMINpassword
+      name: server.example.com
+      hidden: yes
+```
+
+
+Example playbook to make sure server "server.example.com" is present and not hidden:
+
+```yaml
+---
+- name: Playbook to manage IPA server.
+  hosts: ipaserver
+  become: yes
+
+  tasks:
+  - ipaserver:
+      ipaadmin_password: SomeADMINpassword
+      name: server.example.com
+      hidden: no
+```
+
+
+Example playbook to make sure server "server.example.com" is absent:
+
+```yaml
+---
+- name: Playbook to manage IPA server.
+  hosts: ipaserver
+  become: yes
+
+  tasks:
+  - ipaserver:
+      ipaadmin_password: SomeADMINpassword
+      name: server.example.com
+      state: absent
+```
+
+
+Example playbook to make sure server "server.example.com" is absent in continuous mode in error case:
+
+```yaml
+---
+- name: Playbook to manage IPA server.
+  hosts: ipaserver
+  become: yes
+
+  tasks:
+  - ipaserver:
+      ipaadmin_password: SomeADMINpassword
+      name: server.example.com
+      continue: yes
+      state: absent
+```
+
+
+Example playbook to make sure server "server.example.com" is absent with last of role check skip:
+
+```yaml
+---
+- name: Playbook to manage IPA server.
+  hosts: ipaserver
+  become: yes
+
+  tasks:
+  - ipaserver:
+      ipaadmin_password: SomeADMINpassword
+      name: server.example.com
+      ignore_last_of_role: yes
+      state: absent
+```
+
+
+Example playbook to make sure server "server.example.com" is absent iwith topology disconnect check skip:
+
+```yaml
+---
+- name: Playbook to manage IPA server.
+  hosts: ipaserver
+  become: yes
+
+  tasks:
+  - ipaserver:
+      ipaadmin_password: SomeADMINpassword
+      name: server.example.com
+      ignore_topology_disconnect: yes
+      state: absent
+```
+
+
+MORE EXAMPLE PLAYBOOKS HERE
+
+
+Variables
+---------
+
+ipaserver
+-------
+
+Variable | Description | Required
+-------- | ----------- | --------
+`ipaadmin_principal` | The admin principal is a string and defaults to `admin` | no
+`ipaadmin_password` | The admin password is a string and is required if there is no admin ticket available on the node | no
+`name` \| `cn` | The list of server name strings. | yes
+`location` \| `ipalocation_location` | The server location string. Only in state: present. "" for location reset. | no
+`service_weight` \| `ipaserviceweight` | Weight for server services. Type Values 0 to 65535, -1 for weight reset. Only in state: present. (int) | no
+`hidden` | Set hidden state of a server. Only in state: present. (bool) | no
+`no_members` | Suppress processing of membership attributes. Only in state: present. (bool) | no
+`delete_continue` \| `continue` | Continuous mode: Don't stop on errors. Only in state: absent. (bool) | no
+`ignore_last_of_role` | Skip a check whether the last CA master or DNS server is removed. Only in state: absent. (bool) | no
+`ignore_topology_disconnect` | Ignore topology connectivity problems after removal. Only in state: absent. (bool) | no
+`force` | Force server removal even if it does not exist. Will always result in changed. Only in state: absent. (bool) | no
+`state` | The state to ensure. It can be one of `present`, `absent`, default: `present`. `present` is only working with existing servers. | no
+
+
+Authors
+=======
+
+Thomas Woerner
diff --git a/README.md b/README.md
index f0b6d6088e6be2d32a6a15c8ac55ca970967d6dc..6839c31e651887b5b3772904bdac56d93926b03a 100644
--- a/README.md
+++ b/README.md
@@ -30,6 +30,7 @@ Features
 * Modules for pwpolicy management
 * Modules for role management
 * Modules for self service management
+* Modules for server management
 * Modules for service management
 * Modules for sudocmd management
 * Modules for sudocmdgroup management
@@ -439,6 +440,7 @@ Modules in plugin/modules
 * [ipapwpolicy](README-pwpolicy.md)
 * [iparole](README-role.md)
 * [ipaselfservice](README-ipaselfservice.md)
+* [ipaserver](README-server.md)
 * [ipaservice](README-service.md)
 * [ipasudocmd](README-sudocmd.md)
 * [ipasudocmdgroup](README-sudocmdgroup.md)
diff --git a/playbooks/server/server-absent-continue.yml b/playbooks/server/server-absent-continue.yml
new file mode 100644
index 0000000000000000000000000000000000000000..40bba4a5e50e0175330dce5e1e17d5845eec5f87
--- /dev/null
+++ b/playbooks/server/server-absent-continue.yml
@@ -0,0 +1,12 @@
+---
+- name: Server absent continuous mode example
+  hosts: ipaserver
+  become: true
+
+  tasks:
+  - name: Ensure server "absent.example.com" is absent continuous mode
+    ipaserver:
+      ipaadmin_password: SomeADMINpassword
+      name: absent.example.com
+      continue: yes
+      state: absent
diff --git a/playbooks/server/server-absent-force.yml b/playbooks/server/server-absent-force.yml
new file mode 100644
index 0000000000000000000000000000000000000000..702c405358bf37d2c9e86c11339473d2b0f479b8
--- /dev/null
+++ b/playbooks/server/server-absent-force.yml
@@ -0,0 +1,12 @@
+---
+- name: Server absent with force example
+  hosts: ipaserver
+  become: true
+
+  tasks:
+  - name: Ensure server "absent.example.com" is absent with force
+    ipaserver:
+      ipaadmin_password: SomeADMINpassword
+      name: absent.example.com
+      force: yes
+      state: absent
diff --git a/playbooks/server/server-absent-ignore_last_of_role.yml b/playbooks/server/server-absent-ignore_last_of_role.yml
new file mode 100644
index 0000000000000000000000000000000000000000..364896044dd6bac0ec3c6e25f42a2e66a26821df
--- /dev/null
+++ b/playbooks/server/server-absent-ignore_last_of_role.yml
@@ -0,0 +1,12 @@
+---
+- name: Server absent with last of role skip example
+  hosts: ipaserver
+  become: true
+
+  tasks:
+  - name: Ensure server "absent.example.com" is absent with last of role skip
+    ipaserver:
+      ipaadmin_password: SomeADMINpassword
+      name: absent.example.com
+      ignore_last_of_role: yes
+      state: absent
diff --git a/playbooks/server/server-absent-ignore_topology_disconnect.yml b/playbooks/server/server-absent-ignore_topology_disconnect.yml
new file mode 100644
index 0000000000000000000000000000000000000000..cc76441c8e835df64acd752e2aecabb599d3dfc3
--- /dev/null
+++ b/playbooks/server/server-absent-ignore_topology_disconnect.yml
@@ -0,0 +1,12 @@
+---
+- name: Server absent with ignoring topology disconnects example
+  hosts: ipaserver
+  become: true
+
+  tasks:
+  - name: Ensure server "absent.example.com" is absent with ignoring topology disconnects
+    ipaserver:
+      ipaadmin_password: SomeADMINpassword
+      name: absent.example.com
+      ignore_topology_disconnect: yes
+      state: absent
diff --git a/playbooks/server/server-absent.yml b/playbooks/server/server-absent.yml
new file mode 100644
index 0000000000000000000000000000000000000000..428d83705ad3aac6ef568e8f51c53bdcd9afe64d
--- /dev/null
+++ b/playbooks/server/server-absent.yml
@@ -0,0 +1,11 @@
+---
+- name: Server absent example
+  hosts: ipaserver
+  become: true
+
+  tasks:
+  - name: Ensure server "absent.example.com" is absent
+    ipaserver:
+      ipaadmin_password: SomeADMINpassword
+      name: absent.example.com
+      state: absent
diff --git a/playbooks/server/server-hidden.yml b/playbooks/server/server-hidden.yml
new file mode 100644
index 0000000000000000000000000000000000000000..0939ec38aece8ff185e6134d3698ff13e4634230
--- /dev/null
+++ b/playbooks/server/server-hidden.yml
@@ -0,0 +1,11 @@
+---
+- name: Server hidden example
+  hosts: ipaserver
+  become: true
+
+  tasks:
+  - name: Ensure server "ipareplica1.example.com" is hidden
+    ipaserver:
+      ipaadmin_password: SomeADMINpassword
+      name: ipareplica1.example.com
+      hidden: True
diff --git a/playbooks/server/server-location.yml b/playbooks/server/server-location.yml
new file mode 100644
index 0000000000000000000000000000000000000000..99ce16fc025575cf1ee8af825a712c9ca10d09f2
--- /dev/null
+++ b/playbooks/server/server-location.yml
@@ -0,0 +1,11 @@
+---
+- name: Server enabled example
+  hosts: ipaserver
+  become: true
+
+  tasks:
+  - name: Ensure server "{{ 'ipareplica1.' + ipaserver_domain }}" with location "mylocation"
+    ipaserver:
+      ipaadmin_password: SomeADMINpassword
+      name: "{{ 'ipareplica1.' + ipaserver_domain }}"
+      location: "mylocation"
diff --git a/playbooks/server/server-no-location.yml b/playbooks/server/server-no-location.yml
new file mode 100644
index 0000000000000000000000000000000000000000..87c0bdfdaa7dc175440c93b2ec4cd9cdde687f29
--- /dev/null
+++ b/playbooks/server/server-no-location.yml
@@ -0,0 +1,11 @@
+---
+- name: Server no location example
+  hosts: ipaserver
+  become: true
+
+  tasks:
+  - name: Ensure server "ipareplica1.example.com" with no location
+    ipaserver:
+      ipaadmin_password: SomeADMINpassword
+      name: ipareplica1.example.com
+      location: ""
diff --git a/playbooks/server/server-no-service-weight.yml b/playbooks/server/server-no-service-weight.yml
new file mode 100644
index 0000000000000000000000000000000000000000..716214c5ec4d74468919389bbc0ed79e499d7d25
--- /dev/null
+++ b/playbooks/server/server-no-service-weight.yml
@@ -0,0 +1,11 @@
+---
+- name: Server service weight example
+  hosts: ipaserver
+  become: true
+
+  tasks:
+  - name: Ensure server "ipareplica1.example.com" with no service weight
+    ipaserver:
+      ipaadmin_password: SomeADMINpassword
+      name: ipareplica1.example.com
+      service_weight: -1
diff --git a/playbooks/server/server-not-hidden.yml b/playbooks/server/server-not-hidden.yml
new file mode 100644
index 0000000000000000000000000000000000000000..fd8a01c89c2dfb7d1590d695d3a3fd458234a61b
--- /dev/null
+++ b/playbooks/server/server-not-hidden.yml
@@ -0,0 +1,11 @@
+---
+- name: Server not hidden example
+  hosts: ipaserver
+  become: true
+
+  tasks:
+  - name: Ensure server "ipareplica1.example.com" is not hidden
+    ipaserver:
+      ipaadmin_password: SomeADMINpassword
+      name: ipareplica1.example.com
+      hidden: no
diff --git a/playbooks/server/server-present.yml b/playbooks/server/server-present.yml
new file mode 100644
index 0000000000000000000000000000000000000000..7937c169b8ab56c9fac85f7217c72ba04552ba07
--- /dev/null
+++ b/playbooks/server/server-present.yml
@@ -0,0 +1,10 @@
+---
+- name: Server present example
+  hosts: ipaserver
+  become: true
+
+  tasks:
+  - name: Ensure server "ipareplica1.exmple.com" is present
+    ipaserver:
+      ipaadmin_password: SomeADMINpassword
+      name: ipareplica1.example.com
diff --git a/playbooks/server/server-service-weight.yml b/playbooks/server/server-service-weight.yml
new file mode 100644
index 0000000000000000000000000000000000000000..7a47edc03ff436cf9452416ace4f67c844e1ac2e
--- /dev/null
+++ b/playbooks/server/server-service-weight.yml
@@ -0,0 +1,11 @@
+---
+- name: Server service weight example
+  hosts: ipaserver
+  become: true
+
+  tasks:
+  - name: Ensure server "ipareplica1.example.com" with service weight 1
+    ipaserver:
+      ipaadmin_password: SomeADMINpassword
+      name: ipareplica1.example.com
+      service_weight: 1
diff --git a/plugins/module_utils/ansible_freeipa_module.py b/plugins/module_utils/ansible_freeipa_module.py
index 32098b351a05c16fb0456481730d19a2cba93f3d..cf62b026a5580c7771e4982a13788019854e41e3 100644
--- a/plugins/module_utils/ansible_freeipa_module.py
+++ b/plugins/module_utils/ansible_freeipa_module.py
@@ -26,7 +26,7 @@ __all__ = ["gssapi", "netaddr", "api", "ipalib_errors", "Env",
            "DEFAULT_CONFIG", "LDAP_GENERALIZED_TIME_FORMAT",
            "kinit_password", "kinit_keytab", "run", "DN", "VERSION",
            "paths", "get_credentials_if_valid", "Encoding",
-           "load_pem_x509_certificate"]
+           "load_pem_x509_certificate", "DNSName"]
 
 import sys
 
@@ -81,6 +81,7 @@ else:
     from ipapython.version import VERSION
     from ipaplatform.paths import paths
     from ipalib.krb_utils import get_credentials_if_valid
+    from ipapython.dnsutil import DNSName
     from ansible.module_utils.basic import AnsibleModule
     from ansible.module_utils._text import to_text
     from ansible.module_utils.common.text.converters import jsonify
diff --git a/plugins/modules/ipaserver.py b/plugins/modules/ipaserver.py
new file mode 100644
index 0000000000000000000000000000000000000000..167394e372bd1f6de15bc82009e8cf9fc43f418a
--- /dev/null
+++ b/plugins/modules/ipaserver.py
@@ -0,0 +1,440 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Authors:
+#   Thomas Woerner <twoerner@redhat.com>
+#
+# Copyright (C) 2021 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: ipaserver
+short description: Manage FreeIPA server
+description: Manage FreeIPA server
+options:
+  ipaadmin_principal:
+    description: The admin principal.
+    default: admin
+  ipaadmin_password:
+    description: The admin password.
+    required: false
+  name:
+    description: The list of server name strings.
+    required: true
+    aliases: ["cn"]
+  location:
+    description: |
+      The server location string.
+      "" for location reset.
+      Only in state: present.
+    required: false
+    aliases: ["ipalocation_location"]
+  service_weight:
+    description: |
+      Weight for server services
+      Values 0 to 65535, -1 for weight reset.
+      Only in state: present.
+    required: false
+    type: int
+    aliases: ["ipaserviceweight"]
+  hidden:
+    description: |
+      Set hidden state of a server.
+      Only in state: present.
+    required: false
+    type: bool
+  no_members:
+    description: |
+      Suppress processing of membership attributes
+      Only in state: present.
+    required: false
+    type: bool
+  delete_continue:
+    description: |
+      Continuous mode: Don't stop on errors.
+      Only in state: absent.
+    required: false
+    type: bool
+    aliases: ["continue"]
+  ignore_last_of_role:
+    description: |
+      Skip a check whether the last CA master or DNS server is removed.
+      Only in state: absent.
+    required: false
+    type: bool
+  ignore_topology_disconnect:
+    description: |
+      Ignore topology connectivity problems after removal.
+      Only in state: absent.
+    required: false
+    type: bool
+  force:
+    description: |
+      Force server removal even if it does not exist.
+      Will always result in changed.
+      Only in state: absent.
+    required: false
+    type: bool
+  state:
+    description: The state to ensure.
+    choices: ["present", "absent"]
+    default: present
+    required: true
+"""
+
+EXAMPLES = """
+# Ensure server server.example.com is present
+- ipaserver:
+    ipaadmin_password: SomeADMINpassword
+    name: server.example.com
+
+# Ensure server server.example.com is absent
+- ipaserver:
+    ipaadmin_password: SomeADMINpassword
+    name: server.example.com
+    state: absent
+
+# Ensure server server.example.com is present with location mylocation
+- ipaserver:
+    ipaadmin_password: SomeADMINpassword
+    name: server.example.com
+    location: mylocation
+
+# Ensure server server.example.com is present without a location
+- ipaserver:
+    ipaadmin_password: SomeADMINpassword
+    name: server.example.com
+    location: ""
+
+# Ensure server server.example.com is present with service weight 1
+- ipaserver:
+    ipaadmin_password: SomeADMINpassword
+    name: server.example.com
+    service_weight: 1
+
+# Ensure server server.example.com is present without service weight
+- ipaserver:
+    ipaadmin_password: SomeADMINpassword
+    name: server.example.com
+    service_weight: -1
+
+# Ensure server server.example.com is present and hidden
+- ipaserver:
+    ipaadmin_password: SomeADMINpassword
+    name: server.example.com
+    hidden: yes
+
+# Ensure server server.example.com is present and not hidden
+- ipaserver:
+    ipaadmin_password: SomeADMINpassword
+    name: server.example.com
+    hidden: no
+
+# Ensure server server.example.com is absent in continuous mode in error case
+- ipaserver:
+    ipaadmin_password: SomeADMINpassword
+    name: server.example.com
+    continue: yes
+    state: absent
+
+# Ensure server server.example.com is absent with last of role check skip
+- ipaserver:
+    ipaadmin_password: SomeADMINpassword
+    name: server.example.com
+    ignore_last_of_role: yes
+    state: absent
+
+# Ensure server server.example.com is absent with topology disconnect check
+# skip
+- ipaserver:
+    ipaadmin_password: SomeADMINpassword
+    name: server.example.com
+    ignore_topology_disconnect: yes
+    state: absent
+
+# Ensure server server.example.com is absent in force mode
+- ipaserver:
+    ipaadmin_password: SomeADMINpassword
+    name: server.example.com
+    force: yes
+    state: absent
+"""
+
+RETURN = """
+"""
+
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible.module_utils.ansible_freeipa_module import \
+    temp_kinit, temp_kdestroy, valid_creds, api_connect, api_command, \
+    api_command_no_name, compare_args_ipa, module_params_get, DNSName
+import six
+
+if six.PY3:
+    unicode = str
+
+
+def find_server(module, name):
+    """Find if a server with the given name already exist."""
+    try:
+        _result = api_command(module, "server_show", name, {"all": True})
+    except Exception:  # pylint: disable=broad-except
+        # An exception is raised if server name is not found.
+        return None
+    else:
+        return _result["result"]
+
+
+def server_role_status(module, name):
+    """Get server role of a hidden server with the given name."""
+    try:
+        _result = api_command_no_name(module, "server_role_find",
+                                      {"server_server": name,
+                                       "role_servrole": 'IPA master',
+                                       "include_master": True,
+                                       "raw": True,
+                                       "all": True})
+    except Exception:  # pylint: disable=broad-except
+        # An exception is raised if server name is not found.
+        return None
+    else:
+        return _result["result"][0]
+
+
+def gen_args(location, service_weight, no_members, delete_continue,
+             ignore_topology_disconnect, ignore_last_of_role, force):
+    _args = {}
+    if location is not None:
+        if location != "":
+            _args["ipalocation_location"] = DNSName(location)
+        else:
+            _args["ipalocation_location"] = None
+    if service_weight is not None:
+        _args["ipaserviceweight"] = service_weight
+    if no_members is not None:
+        _args["no_members"] = no_members
+    if delete_continue is not None:
+        _args["continue"] = delete_continue
+    if ignore_topology_disconnect is not None:
+        _args["ignore_topology_disconnect"] = ignore_topology_disconnect
+    if ignore_last_of_role is not None:
+        _args["ignore_last_of_role"] = ignore_last_of_role
+    if force is not None:
+        _args["force"] = force
+
+    return _args
+
+
+def main():
+    ansible_module = AnsibleModule(
+        argument_spec=dict(
+            # general
+            ipaadmin_principal=dict(type="str", default="admin"),
+            ipaadmin_password=dict(type="str", required=False, no_log=True),
+
+            name=dict(type="list", aliases=["cn"],
+                      default=None, required=True),
+            # present
+            location=dict(required=False, type='str',
+                          aliases=["ipalocation_location"], default=None),
+            service_weight=dict(required=False, type='int',
+                                aliases=["ipaserviceweight"], default=None),
+            hidden=dict(required=False, type='bool', default=None),
+            no_members=dict(required=False, type='bool', default=None),
+            # absent
+            delete_continue=dict(required=False, type='bool',
+                                 aliases=["continue"], default=None),
+            ignore_topology_disconnect=dict(required=False, type='bool',
+                                            default=None),
+            ignore_last_of_role=dict(required=False, type='bool',
+                                     default=None),
+            force=dict(required=False, type='bool',
+                       default=None),
+            # state
+            state=dict(type="str", default="present",
+                       choices=["present", "absent"]),
+        ),
+        supports_check_mode=True,
+    )
+
+    ansible_module._ansible_debug = True
+
+    # Get parameters
+
+    # general
+    ipaadmin_principal = module_params_get(ansible_module,
+                                           "ipaadmin_principal")
+    ipaadmin_password = module_params_get(ansible_module, "ipaadmin_password")
+    names = module_params_get(ansible_module, "name")
+
+    # present
+    location = module_params_get(ansible_module, "location")
+    service_weight = module_params_get(ansible_module, "service_weight")
+    # Service weight smaller than 0 leads to resetting service weight
+    if service_weight is not None and \
+       (service_weight < -1 or service_weight > 65535):
+        ansible_module.fail_json(
+            msg="service_weight %d is out of range [-1 .. 65535]" %
+            service_weight)
+    if service_weight == -1:
+        service_weight = ""
+    hidden = module_params_get(ansible_module, "hidden")
+    no_members = module_params_get(ansible_module, "no_members")
+
+    # absent
+    delete_continue = module_params_get(ansible_module, "delete_continue")
+    ignore_topology_disconnect = module_params_get(
+        ansible_module, "ignore_topology_disconnect")
+    ignore_last_of_role = module_params_get(ansible_module,
+                                            "ignore_last_of_role")
+    force = module_params_get(ansible_module, "force")
+
+    # state
+    state = module_params_get(ansible_module, "state")
+
+    # Check parameters
+
+    invalid = []
+
+    if state == "present":
+        if len(names) != 1:
+            ansible_module.fail_json(
+                msg="Only one server can be ensured at a time.")
+        invalid = ["delete_continue", "ignore_topology_disconnect",
+                   "ignore_last_of_role", "force"]
+
+    if state == "absent":
+        if len(names) < 1:
+            ansible_module.fail_json(msg="No name given.")
+        invalid = ["location", "service_weight", "hidden", "no_members"]
+
+    for x in invalid:
+        if vars()[x] is not None:
+            ansible_module.fail_json(
+                msg="Argument '%s' can not be used with state '%s'" %
+                (x, state))
+
+    # Init
+
+    changed = False
+    exit_args = {}
+    ccache_dir = None
+    ccache_name = None
+    try:
+        if not valid_creds(ansible_module, ipaadmin_principal):
+            ccache_dir, ccache_name = temp_kinit(ipaadmin_principal,
+                                                 ipaadmin_password)
+        api_connect()
+
+        commands = []
+        for name in names:
+            # Make sure server exists
+            res_find = find_server(ansible_module, name)
+
+            # Generate args
+            args = gen_args(location, service_weight, no_members,
+                            delete_continue, ignore_topology_disconnect,
+                            ignore_last_of_role, force)
+
+            # Create command
+            if state == "present":
+                # Server not found
+                if res_find is None:
+                    ansible_module.fail_json(
+                        msg="Server '%s' not found" % name)
+
+                # Remove location from args if "" (transformed to None)
+                # and "ipalocation_location" not in res_find for idempotency
+                if "ipalocation_location" in args and \
+                   args["ipalocation_location"] is None and \
+                   "ipalocation_location" not in res_find:
+                    del args["ipalocation_location"]
+
+                # Remove service weight from args if ""
+                # and "ipaserviceweight" not in res_find for idempotency
+                if "ipaserviceweight" in args and \
+                   args["ipaserviceweight"] == "" and \
+                   "ipaserviceweight" not in res_find:
+                    del args["ipaserviceweight"]
+
+                # For all settings is args, check if there are
+                # different settings in the find result.
+                # If yes: modify
+                if not compare_args_ipa(ansible_module, args, res_find):
+                    commands.append([name, "server_mod", args])
+
+                # hidden handling
+                if hidden is not None:
+                    res_role_status = server_role_status(ansible_module,
+                                                         name)
+
+                    if "status" in res_role_status:
+                        # Fail if status is configured, it should be done
+                        # only in the installer
+                        if res_role_status["status"] == "configured":
+                            ansible_module.fail_json(
+                                msg="'%s' in configured state, "
+                                "unable to change state" % state)
+
+                        if hidden and res_role_status["status"] == "enabled":
+                            commands.append([name, "server_state",
+                                             {"state": "hidden"}])
+                        if not hidden and \
+                           res_role_status["status"] == "hidden":
+                            commands.append([name, "server_state",
+                                             {"state": "enabled"}])
+
+            elif state == "absent":
+                if res_find is not None or force:
+                    commands.append([name, "server_del", args])
+            else:
+                ansible_module.fail_json(msg="Unkown state '%s'" % state)
+
+        # Execute commands
+
+        for name, command, args in commands:
+            try:
+                result = api_command(ansible_module, command, name,
+                                     args)
+                if "completed" in result:
+                    if result["completed"] > 0:
+                        changed = True
+                else:
+                    changed = True
+            except Exception as e:
+                ansible_module.fail_json(msg="%s: %s: %s" % (command, name,
+                                                             str(e)))
+
+    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, **exit_args)
+
+
+if __name__ == "__main__":
+    main()
diff --git a/tests/server/test_server.yml b/tests/server/test_server.yml
new file mode 100644
index 0000000000000000000000000000000000000000..d42a55657ee5a24cd7e53e2c28a11d58d78eb4fa
--- /dev/null
+++ b/tests/server/test_server.yml
@@ -0,0 +1,134 @@
+---
+- name: Test server
+  hosts: ipaserver
+  become: true
+
+  tasks:
+
+  # CLEANUP TEST ITEMS
+
+  - name: Ensure server "{{ 'ipaserver.' + ipaserver_domain }}" without location
+    ipaserver:
+      ipaadmin_password: SomeADMINpassword
+      name: "{{ 'ipaserver.' + ipaserver_domain }}"
+      location: ""
+
+  - name: Ensure server "{{ 'ipaserver.' + ipaserver_domain }}" without service weight
+    ipaserver:
+      ipaadmin_password: SomeADMINpassword
+      name: "{{ 'ipaserver.' + ipaserver_domain }}"
+      service_weight: -1
+
+  - name: Ensure location "mylocation" is absent
+    ipalocation:
+      ipaadmin_password: SomeADMINpassword
+      name: mylocation
+      state: absent
+
+# CREATE TEST ITEMS
+
+  - name: Ensure location "mylocation" is present
+    ipalocation:
+      ipaadmin_password: SomeADMINpassword
+      name: mylocation
+    register: result
+    failed_when: not result.changed or result.failed
+
+  # TESTS
+
+  - name: Ensure server "{{ 'ipaserver.' + ipaserver_domain }}" is present
+    ipaserver:
+      ipaadmin_password: SomeADMINpassword
+      name: "{{ 'ipaserver.' + ipaserver_domain }}"
+    register: result
+    failed_when: result.changed or result.failed
+
+  - name: Ensure server "{{ 'ipaserver.' + ipaserver_domain }}" with location "mylocation"
+    ipaserver:
+      ipaadmin_password: SomeADMINpassword
+      name: "{{ 'ipaserver.' + ipaserver_domain }}"
+      location: "mylocation"
+    register: result
+    failed_when: not result.changed or result.failed
+
+  - name: Ensure server "{{ 'ipaserver.' + ipaserver_domain }}" with location "mylocation" again
+    ipaserver:
+      ipaadmin_password: SomeADMINpassword
+      name: "{{ 'ipaserver.' + ipaserver_domain }}"
+      location: "mylocation"
+    register: result
+    failed_when: result.changed or result.failed
+
+  - name: Ensure server "{{ 'ipaserver.' + ipaserver_domain }}" without location
+    ipaserver:
+      ipaadmin_password: SomeADMINpassword
+      name: "{{ 'ipaserver.' + ipaserver_domain }}"
+      location: ""
+    register: result
+    failed_when: not result.changed or result.failed
+
+  - name: Ensure server "{{ 'ipaserver.' + ipaserver_domain }}" without location again
+    ipaserver:
+      ipaadmin_password: SomeADMINpassword
+      name: "{{ 'ipaserver.' + ipaserver_domain }}"
+      location: ""
+    register: result
+    failed_when: result.changed or result.failed
+
+  - name: Ensure server "{{ 'ipaserver.' + ipaserver_domain }}" with service weight 1
+    ipaserver:
+      ipaadmin_password: SomeADMINpassword
+      name: "{{ 'ipaserver.' + ipaserver_domain }}"
+      service_weight: 1
+    register: result
+    failed_when: not result.changed or result.failed
+
+  - name: Ensure server "{{ 'ipaserver.' + ipaserver_domain }}" with service weight 1 again
+    ipaserver:
+      ipaadmin_password: SomeADMINpassword
+      name: "{{ 'ipaserver.' + ipaserver_domain }}"
+      service_weight: 1
+    register: result
+    failed_when: result.changed or result.failed
+
+  - name: Ensure server "{{ 'ipaserver.' + ipaserver_domain }}" without service weight
+    ipaserver:
+      ipaadmin_password: SomeADMINpassword
+      name: "{{ 'ipaserver.' + ipaserver_domain }}"
+      service_weight: -1
+    register: result
+    failed_when: not result.changed or result.failed
+
+  - name: Ensure server "{{ 'ipaserver.' + ipaserver_domain }}" without service weight again
+    ipaserver:
+      ipaadmin_password: SomeADMINpassword
+      name: "{{ 'ipaserver.' + ipaserver_domain }}"
+      service_weight: -1
+    register: result
+    failed_when: result.changed or result.failed
+
+  # hidden requires an additional server, not tested
+
+  # absent requires an additional server, only sanity test with absent server
+
+  - name: Ensure server "{{ 'absent.' + ipaserver_domain }}" is absent
+    ipaserver:
+      ipaadmin_password: SomeADMINpassword
+      name: "{{ 'absent.' + ipaserver_domain }}"
+      state: absent
+    register: result
+    failed_when: result.changed or result.failed
+
+  # ignore_last_of_role requires an additional server, not tested
+
+  # ignore_topology_disconnect requires an additional server, not tested
+
+  # CLEANUP TEST ITEMS
+
+  - name: Ensure location "mylocation" is absent
+    ipalocation:
+      ipaadmin_password: SomeADMINpassword
+      name: mylocation
+      state: absent
+    register: result
+    failed_when: not result.changed or result.failed