diff --git a/roles/ipareplica/README.md b/roles/ipareplica/README.md index 8d70b45b4f6d31d2e862238e586b632a8c2db76f..fd7388a52a555488b43710d9d7adeae0a43b9b26 100644 --- a/roles/ipareplica/README.md +++ b/roles/ipareplica/README.md @@ -114,6 +114,50 @@ Example playbook to setup the IPA client(s) using principal and password from in state: present ``` +Example inventory file to remove a replica from the domain: + +```ini +[ipareplicas] +ipareplica1.example.com + +[ipareplicas:vars] +ipaadmin_password=MySecretPassword123 +ipareplica_remove_from_domain=true +``` + +Example playbook to remove an IPA replica using admin passwords from the domain: + +```yaml +--- +- name: Playbook to remove IPA replica + hosts: ipareplica + become: true + + roles: + - role: ipareplica + state: absent +``` + +The inventory will enable the removal of the replica (also a replica) from the domain. Additional options are needed if the removal of the replica is resulting in a topology disconnect or if the replica is the last that has a role. + +To continue with the removal with a topology disconnect it is needed to set these parameters: + +```ini +ipareplica_ignore_topology_disconnect=true +ipareplica_remove_on_server=ipareplica2.example.com +``` + +To continue with the removal for a replica that is the last that has a role: + +```ini +ipareplica_ignore_last_of_role=true +``` + +Be careful with enabling the `ipareplica_ignore_topology_disconnect` and especially `ipareplica_ignore_last_of_role`, the change can not be reverted easily. + +The parameters `ipaserver_ignore_topology_disconnect`, `ipaserver_ignore_last_of_role`, `ipaserver_remove_on_server` and `ipaserver_remove_from_domain` can be used instead. + + Playbooks ========= @@ -255,6 +299,19 @@ Variable | Description | Required `ipareplica_setup_firewalld` | The value defines if the needed services will automatically be openen in the firewall managed by firewalld. (bool, default: true) | no `ipareplica_firewalld_zone` | The value defines the firewall zone that will be used. This needs to be an existing runtime and permanent zone. (string) | no +Undeploy Variables (`state`: absent) +------------------------------------ + +These settings should only be used if the result is really wanted. The change might not be revertable easily. + +Variable | Description | Required +-------- | ----------- | -------- +`ipareplica_ignore_topology_disconnect` \| `ipaserver_ignore_topology_disconnect` | If enabled this enforces the removal of the replica even if it results in a topology disconnect. Be careful with this setting. (bool) | false +`ipareplica_ignore_last_of_role` \| `ipaserver_ignore_last_of_role` | If enabled this enforces the removal of the replica even if the replica is the last with one that has a role. Be careful, this might not be revered easily. (bool) | false +`ipareplica_remove_from_domain` \| `ipaserver_remove_from_domain` | This enables the removal of the replica from the domain additionally to the undeployment. (bool) | false +`ipareplica_remove_on_server` \| `ipaserver_remove_on_server` | The value defines the replica in the domain that will to be used to remove the replica from the domain if `ipareplica_ignore_topology_disconnect` and `ipareplica_remove_from_domain` are enabled. Without the need to enable `ipareplica_ignore_topology_disconnect`, the value will be automatically detected using the replication agreements of the replica. (string) | false + + Authors ======= diff --git a/roles/ipareplica/tasks/uninstall.yml b/roles/ipareplica/tasks/uninstall.yml index a5998ece0a7a119b00a821e248a28b1068eae7d0..0b065c3b564c901197272f607d4001f6bc909382 100644 --- a/roles/ipareplica/tasks/uninstall.yml +++ b/roles/ipareplica/tasks/uninstall.yml @@ -1,37 +1,19 @@ --- # tasks to uninstall IPA replica -- name: Uninstall - Uninstall IPA replica - ansible.builtin.command: > - /usr/sbin/ipa-server-install - --uninstall - -U - {{ "--ignore-topology-disconnect" if - ipareplica_ignore_topology_disconnect | bool else "" }} - {{ "--ignore-last-of-role" if ipareplica_ignore_last_of_role | bool - else "" }} - register: result_uninstall - # 2 means that uninstall failed because IPA replica was not configured - failed_when: result_uninstall.rc != 0 and "'Env' object - has no attribute 'basedn'" not in result_uninstall.stderr - # IPA server is not configured on this system" not in - # result_uninstall.stdout_lines - changed_when: result_uninstall.rc == 0 - # until: result_uninstall.rc == 0 - retries: 2 - delay: 1 +- name: Set parameters + ansible.builtin.set_fact: + _ignore_topology_disconnect: "{{ ipaserver_ignore_topology_disconnect | default(ipareplica_ignore_topology_disconnect) | default(omit) }}" + _ignore_last_of_role: "{{ ipaserver_ignore_last_of_role | default(ipareplica_ignore_last_of_role) | default(omit) }}" + _remove_from_domain: "{{ ipaserver_remove_from_domain | default(ipareplica_remove_from_domain) | default(omit) }}" + _remove_on_server: "{{ ipaserver_remove_on_server | default(ipareplica_remove_on_server) | default(omit) }}" -#- name: Uninstall - Remove all replication agreements and data about replica -# ansible.builtin.command: > -# /usr/sbin/ipa-replica-manage -# del -# {{ ipareplica_hostname | default(ansible_facts['fqdn']) }} -# --force -# --password={{ ipadm_password }} -# failed_when: False -# delegate_to: "{{ groups.ipaserver[0] | default(fail) }}" - -#- name: Remove IPA replica packages -# ansible.builtin.package: -# name: "{{ ipareplica_packages }}" -# state: absent +- name: Uninstall - Uninstall replica + ansible.builtin.include_role: + name: ipaserver + vars: + state: absent + ipaserver_ignore_topology_disconnect: "{{ _ignore_topology_disconnect | default(false) }}" + ipaserver_ignore_last_of_role: "{{ _ignore_last_of_role | default(false) }}" + ipaserver_remove_from_domain: "{{ _remove_from_domain | default(false) }}" + ipaserver_remove_on_server: "{{ _remove_on_server | default(NULL) }}" diff --git a/roles/ipaserver/README.md b/roles/ipaserver/README.md index 18317fb9f0bbaf3357724c0fbeecc1ae4242b009..130be07c7b34f3bdcc837564cc85199690e5dcae 100644 --- a/roles/ipaserver/README.md +++ b/roles/ipaserver/README.md @@ -79,7 +79,7 @@ Example playbook to setup the IPA server using admin and dirman passwords from a state: present ``` -Example playbook to unconfigure the IPA client(s) using principal and password from inventory file: +Example playbook to unconfigure the IPA server using principal and password from inventory file: ```yaml --- @@ -169,6 +169,48 @@ Server installation step 2: Copy `<ipaserver hostname>-chain.crt` to the IPA ser The files can also be copied automatically: Set `ipaserver_copy_csr_to_controller` to true in the server installation step 1 and set `ipaserver_external_cert_files_from_controller` to point to the `chain.crt` file in the server installation step 2. +Example inventory file to remove a server from the domain: + +```ini +[ipaserver] +ipaserver.example.com + +[ipaserver:vars] +ipaadmin_password=MySecretPassword123 +ipaserver_remove_from_domain=true +``` + +Example playbook to remove an IPA server using admin passwords from the domain: + +```yaml +--- +- name: Playbook to remove IPA server + hosts: ipaserver + become: true + + roles: + - role: ipaserver + state: absent +``` + +The inventory will enable the removal of the server (also a replica) from the domain. Additional options are needed if the removal of the server/replica is resulting in a topology disconnect or if the server/replica is the last that has a role. + +To continue with the removal with a topology disconnect it is needed to set these parameters: + +```ini +ipaserver_ignore_topology_disconnect=true +ipaserver_remove_on_server=ipaserver2.example.com +``` + +To continue with the removal for a server that is the last that has a role: + +```ini +ipaserver_ignore_last_of_role=true +``` + +Be careful with enabling the `ipaserver_ignore_topology_disconnect` and especially `ipaserver_ignore_last_of_role`, the change can not be reverted easily. + + Playbooks ========= @@ -305,6 +347,19 @@ Variable | Description | Required `ipaserver_external_cert_files_from_controller` | Files containing the IPA CA certificates and the external CA certificate chains on the controller that will be copied to the ipaserver host to `/root` folder. (list of string) | no `ipaserver_copy_csr_to_controller` | Copy the generated CSR from the ipaserver to the controller as `"{{ inventory_hostname }}-ipa.csr"`. (bool) | no +Undeploy Variables (`state`: absent) +------------------------------------ + +These settings should only be used if the result is really wanted. The change might not be revertable easily. + +Variable | Description | Required +-------- | ----------- | -------- +`ipaserver_ignore_topology_disconnect` | If enabled this enforces the removal of the server even if it results in a topology disconnect. Be careful with this setting. (bool) | false +`ipaserver_ignore_last_of_role` | If enabled this enforces the removal of the server even if the server is the last with one that has a role. Be careful, this might not be revered easily. (bool) | false +`ipaserver_remove_from_domain` | This enables the removal of the server from the domain additionally to the undeployment. (bool) | false +`ipaserver_remove_on_server` | The value defines the server/replica in the domain that will to be used to remove the server/replica from the domain if `ipaserver_ignore_topology_disconnect` and `ipaserver_remove_from_domain` are enabled. Without the need to enable `ipaserver_ignore_topology_disconnect`, the value will be automatically detected using the replication agreements of the server/replica. (string) | false + + Authors ======= diff --git a/roles/ipaserver/defaults/main.yml b/roles/ipaserver/defaults/main.yml index 6abcb796fbb22f95d8d141247e8c407201404401..5af85c2d4473a387991fc02d673651e8b4a91f78 100644 --- a/roles/ipaserver/defaults/main.yml +++ b/roles/ipaserver/defaults/main.yml @@ -42,3 +42,4 @@ ipaserver_copy_csr_to_controller: no ### uninstall ### ipaserver_ignore_topology_disconnect: no ipaserver_ignore_last_of_role: no +ipaserver_remove_from_domain: false diff --git a/roles/ipaserver/library/ipaserver_get_connected_server.py b/roles/ipaserver/library/ipaserver_get_connected_server.py new file mode 100644 index 0000000000000000000000000000000000000000..910104bbb4d516bf6e1971002f64700ad2fed5f3 --- /dev/null +++ b/roles/ipaserver/library/ipaserver_get_connected_server.py @@ -0,0 +1,264 @@ +# -*- coding: utf-8 -*- + +# Authors: +# Thomas Woerner <twoerner@redhat.com> +# +# Copyright (C) 2019-2022 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/>. + +from __future__ import (absolute_import, division, print_function) + +__metaclass__ = type + +ANSIBLE_METADATA = { + "metadata_version": "1.0", + "supported_by": "community", + "status": ["preview"], +} + +DOCUMENTATION = """ +--- +module: ipaserver_get_connected_server +short_description: Get connected servers for server +description: Get connected servers for server +options: + ipaadmin_principal: + description: The admin principal. + default: admin + type: str + ipaadmin_password: + description: The admin password. + required: true + type: str + hostname: + description: The FQDN server name. + type: str + required: true +author: + - Thomas Woerner (@t-woerner) +""" + +EXAMPLES = """ +""" + +RETURN = """ +server: + description: Connected server name + returned: always + type: str +""" + +import os +import tempfile +import shutil +from contextlib import contextmanager +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils._text import to_text +from ansible.module_utils import six + +try: + from ipalib import api + from ipalib import errors as ipalib_errors # noqa + from ipalib.config import Env + from ipaplatform.paths import paths + from ipapython.ipautil import run + from ipalib.constants import DEFAULT_CONFIG + try: + from ipalib.install.kinit import kinit_password + except ImportError: + from ipapython.ipautil import kinit_password +except ImportError as _err: + MODULE_IMPORT_ERROR = str(_err) +else: + MODULE_IMPORT_ERROR = None + + +if six.PY3: + unicode = str + + +def temp_kinit(principal, password): + """Kinit with password using a temporary ccache.""" + 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: %s" % str(e)) + + os.environ["KRB5CCNAME"] = ccache_name + 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) + del os.environ['KRB5CCNAME'] + if ccache_dir is not None: + shutil.rmtree(ccache_dir, ignore_errors=True) + + +@contextmanager +def ipa_connect(module, principal=None, password=None): + """ + Create a context with a connection to IPA API. + + Parameters + ---------- + module: AnsibleModule + The AnsibleModule to use + principal: string + The optional principal name + password: string + The admin password. + + """ + if not password: + module.fail_json(msg="Password is required.") + if not principal: + principal = "admin" + + ccache_dir = None + ccache_name = None + try: + ccache_dir, ccache_name = temp_kinit(principal, password) + # api_connect start + env = Env() + env._bootstrap() + env._finalize_core(**dict(DEFAULT_CONFIG)) + + api.bootstrap(context="server", debug=env.debug, log=None) + api.finalize() + + if api.env.in_server: + backend = api.Backend.ldap2 + else: + backend = api.Backend.rpcclient + + if not backend.isconnected(): + backend.connect(ccache=ccache_name) + # api_connect end + except Exception as e: + module.fail_json(msg=str(e)) + else: + try: + yield ccache_name + except Exception as e: + module.fail_json(msg=str(e)) + finally: + temp_kdestroy(ccache_dir, ccache_name) + + +def ipa_command(command, name, args): + """ + Execute an IPA API command with a required `name` argument. + + Parameters + ---------- + command: string + The IPA API command to execute. + name: string + The name parameter to pass to the command. + args: dict + The parameters to pass to the command. + + """ + return api.Command[command](name, **args) + + +def _afm_convert(value): + if value is not None: + if isinstance(value, list): + return [_afm_convert(x) for x in value] + if isinstance(value, dict): + return {_afm_convert(k): _afm_convert(v) + for k, v in value.items()} + if isinstance(value, str): + return to_text(value) + + return value + + +def module_params_get(module, name): + return _afm_convert(module.params.get(name)) + + +def host_show(module, name): + _args = { + "all": True, + } + + try: + _result = ipa_command("host_show", name, _args) + except ipalib_errors.NotFound as e: + msg = str(e) + if "host not found" in msg: + return None + module.fail_json(msg="host_show failed: %s" % msg) + + return _result["result"] + + +def main(): + module = AnsibleModule( + argument_spec=dict( + ipaadmin_principal=dict(type="str", default="admin"), + ipaadmin_password=dict(type="str", required=True, no_log=True), + hostname=dict(type="str", required=True), + ), + supports_check_mode=True, + ) + + if MODULE_IMPORT_ERROR is not None: + module.fail_json(msg=MODULE_IMPORT_ERROR) + + # In check mode always return changed. + if module.check_mode: + module.exit_json(changed=False) + + ipaadmin_principal = module_params_get(module, "ipaadmin_principal") + ipaadmin_password = module_params_get(module, "ipaadmin_password") + hostname = module_params_get(module, "hostname") + + server = None + right_left = ["iparepltoposegmentrightnode", "iparepltoposegmentleftnode"] + with ipa_connect(module, ipaadmin_principal, ipaadmin_password): + # At first search in the domain, then ca suffix: + # Search for the first iparepltoposegmentleftnode (node 2), where + # iparepltoposegmentrightnode is hostname (node 1), then for the + # first iparepltoposegmentrightnode (node 2) where + # iparepltoposegmentleftnode is hostname (node 1). + for suffix_name in ["domain", "ca"]: + for node1, node2 in [[right_left[0], right_left[1]], + [right_left[1], right_left[0]]]: + args = {node1: hostname} + result = api.Command.topologysegment_find( + suffix_name, **args) + if result and "result" in result and len(result["result"]) > 0: + res = result["result"][0] + if node2 in res: + if len(res[node2]) > 0: + server = res[node2][0] + break + if server is not None: + module.exit_json(changed=False, server=server) + module.exit_json(changed=False) + + +if __name__ == "__main__": + main() diff --git a/roles/ipaserver/tasks/uninstall.yml b/roles/ipaserver/tasks/uninstall.yml index 7b69f229946e2f7de4641b49b38719aebe75cf60..35ab63567a39d7e667032925ce26f3256a6e7be0 100644 --- a/roles/ipaserver/tasks/uninstall.yml +++ b/roles/ipaserver/tasks/uninstall.yml @@ -1,6 +1,47 @@ --- # tasks to uninstall IPA server +- name: Uninstall - Set server hostname for removal + ansible.builtin.set_fact: + _remove_hostname: "{{ ansible_facts['fqdn'] }}" + +- name: Uninstall - Remove server + when: ipaserver_remove_from_domain + block: + + - name: Uninstall - Fail on missing ipaadmin_password for server removal + ansible.builtin.fail: + msg: "'ipaadmin_password' is needed for 'ipaserver_remove_from_domain'" + when: ipaadmin_password is not defined + + - name: Uninstall - Fail on missing ipaserver_remove_on_server with ipaserver_ignore_topology_disconnect + ansible.builtin.fail: + msg: "'ipaserver_remove_on_server' is needed for 'ipaserver_remove_from_domain' with 'ipaserver_ignore_topology_disconnect'" + when: ipaserver_ignore_topology_disconnect | bool + and ipaserver_remove_on_server is not defined + + - name: Uninstall - Get connected server + ipaserver_get_connected_server: + ipaadmin_principal: "{{ ipaadmin_principal | default('admin') }}" + ipaadmin_password: "{{ ipaadmin_password }}" + hostname: "{{ _remove_hostname }}" + register: result_get_connected_server + when: ipaserver_remove_on_server is not defined + + # REMOVE SERVER FROM DOMAIN + - name: Uninstall - Server del "{{ _remove_hostname }}" + ipaserver: + ipaadmin_principal: "{{ ipaadmin_principal | default('admin') }}" + ipaadmin_password: "{{ ipaadmin_password }}" + name: "{{ _remove_hostname }}" + ignore_last_of_role: "{{ ipaserver_ignore_last_of_role }}" + ignore_topology_disconnect: "{{ ipaserver_ignore_topology_disconnect }}" + # delete_continue: "{{ ipaserver_delete_continue }}" + state: absent + delegate_to: "{{ ipaserver_remove_on_server | default(result_get_connected_server.server) }}" + when: ipaserver_remove_on_server is defined or + result_get_connected_server.server is defined + - name: Uninstall - Uninstall IPA server ansible.builtin.command: > /usr/sbin/ipa-server-install