Blame SOURCES/ansible-freeipa-ipahost-Add-support-for-several-IP-addresses-and-also-to-change-them_rhbz#1783979,1783976.patch

d9912c
From 167c76311da72c2bfabf4b2bce9e128c11d519d0 Mon Sep 17 00:00:00 2001
d9912c
From: Thomas Woerner <twoerner@redhat.com>
d9912c
Date: Wed, 12 Feb 2020 16:54:13 +0100
d9912c
Subject: [PATCH] ipahost: Add support for several IP addresses and also to
d9912c
 change them
d9912c
d9912c
ipahost was so far ignoring IP addresses when the host already existed.
d9912c
This happened because host_mod is not providing functionality to do this.
d9912c
Now ipaddress is a list and it is possible to ensure a host with several
d9912c
IP addresses (these can be IPv4 and IPv6). Also it is possible to ensure
d9912c
presence and absence of IP addresses for an exising host using action
d9912c
member.
d9912c
d9912c
There are no IP address conclict checks as this would lead into issues with
d9912c
updating an existing host that already is using a duplicate IP address for
d9912c
example for round-robin (RR). Also this might lead into issues with ensuring
d9912c
a new host with several IP addresses in this case. Also to ensure a list of
d9912c
hosts with changing the IP address of one host to another in the list would
d9912c
result in issues here.
d9912c
d9912c
New example playbooks have been added:
d9912c
d9912c
    playbooks/host/host-present-with-several-ip-addresses.yml
d9912c
    playbooks/host/host-member-ipaddresses-absent.yml
d9912c
    playbooks/host/host-member-ipaddresses-present.yml
d9912c
d9912c
A new test has been added for verification:
d9912c
d9912c
    tests/host/test_host_ipaddresses.yml
d9912c
d9912c
Fixes: https://bugzilla.redhat.com/show_bug.cgi?id=1783976
d9912c
       https://bugzilla.redhat.com/show_bug.cgi?id=1783979
d9912c
---
d9912c
 README-host.md                                |  79 ++++-
d9912c
 .../host/host-member-ipaddresses-absent.yml   |  17 +
d9912c
 .../host/host-member-ipaddresses-present.yml  |  16 +
d9912c
 ...host-present-with-several-ip-addresses.yml |  24 ++
d9912c
 .../module_utils/ansible_freeipa_module.py    |  23 ++
d9912c
 plugins/modules/ipahost.py                    | 179 +++++++---
d9912c
 tests/host/test_host_ipaddresses.yml          | 312 ++++++++++++++++++
d9912c
 7 files changed, 600 insertions(+), 50 deletions(-)
d9912c
 create mode 100644 playbooks/host/host-member-ipaddresses-absent.yml
d9912c
 create mode 100644 playbooks/host/host-member-ipaddresses-present.yml
d9912c
 create mode 100644 playbooks/host/host-present-with-several-ip-addresses.yml
d9912c
 create mode 100644 tests/host/test_host_ipaddresses.yml
d9912c
d9912c
diff --git a/README-host.md b/README-host.md
d9912c
index be5ad79..ecc59a9 100644
d9912c
--- a/README-host.md
d9912c
+++ b/README-host.md
d9912c
@@ -65,6 +65,79 @@ Example playbook to ensure host presence:
d9912c
       - "52:54:00:BD:97:1E"
d9912c
       state: present
d9912c
 ```
d9912c
+Compared to `ipa host-add` command no IP address conflict check is done as the ipahost module supports to have several IPv4 and IPv6 addresses for a host.
d9912c
+
d9912c
+
d9912c
+Example playbook to ensure host presence with several IP addresses:
d9912c
+
d9912c
+```yaml
d9912c
+---
d9912c
+- name: Playbook to handle hosts
d9912c
+  hosts: ipaserver
d9912c
+  become: true
d9912c
+
d9912c
+  tasks:
d9912c
+  # Ensure host is present
d9912c
+  - ipahost:
d9912c
+      ipaadmin_password: MyPassword123
d9912c
+      name: host01.example.com
d9912c
+      description: Example host
d9912c
+      ip_address:
d9912c
+      - 192.168.0.123
d9912c
+      - 192.168.0.124
d9912c
+      - fe80::20c:29ff:fe02:a1b3
d9912c
+      - fe80::20c:29ff:fe02:a1b4
d9912c
+      locality: Lab
d9912c
+      ns_host_location: Lab
d9912c
+      ns_os_version: CentOS 7
d9912c
+      ns_hardware_platform: Lenovo T61
d9912c
+      mac_address:
d9912c
+      - "08:00:27:E3:B1:2D"
d9912c
+      - "52:54:00:BD:97:1E"
d9912c
+      state: present
d9912c
+```
d9912c
+
d9912c
+
d9912c
+Example playbook to ensure IP addresses are present for a host:
d9912c
+
d9912c
+```yaml
d9912c
+---
d9912c
+- name: Playbook to handle hosts
d9912c
+  hosts: ipaserver
d9912c
+  become: true
d9912c
+
d9912c
+  tasks:
d9912c
+  # Ensure host is present
d9912c
+  - ipahost:
d9912c
+      ipaadmin_password: MyPassword123
d9912c
+      name: host01.example.com
d9912c
+      ip_address:
d9912c
+      - 192.168.0.124
d9912c
+      - fe80::20c:29ff:fe02:a1b4
d9912c
+      action: member
d9912c
+      state: present
d9912c
+```
d9912c
+
d9912c
+
d9912c
+Example playbook to ensure IP addresses are absent for a host:
d9912c
+
d9912c
+```yaml
d9912c
+---
d9912c
+- name: Playbook to handle hosts
d9912c
+  hosts: ipaserver
d9912c
+  become: true
d9912c
+
d9912c
+  tasks:
d9912c
+  # Ensure host is present
d9912c
+  - ipahost:
d9912c
+      ipaadmin_password: MyPassword123
d9912c
+      name: host01.example.com
d9912c
+      ip_address:
d9912c
+      - 192.168.0.124
d9912c
+      - fe80::20c:29ff:fe02:a1b4
d9912c
+      action: member
d9912c
+      state: absent
d9912c
+```
d9912c
 
d9912c
 
d9912c
 Example playbook to ensure host presence without DNS:
d9912c
@@ -215,7 +288,7 @@ Example playbook to disable a host:
d9912c
       update_dns: yes
d9912c
       state: disabled
d9912c
 ```
d9912c
-`update_dns` controls if the DNS entries will be updated.
d9912c
+`update_dns` controls if the DNS entries will be updated in this case. For `state` present it is controlling the update of the DNS SSHFP records, but not the the other DNS records.
d9912c
 
d9912c
 
d9912c
 Example playbook to ensure a host is absent:
d9912c
@@ -286,8 +359,8 @@ Variable | Description | Required
d9912c
 `ok_to_auth_as_delegate` \| `ipakrboktoauthasdelegate` | The service is allowed to authenticate on behalf of a client (bool) | no
d9912c
 `force` | Force host name even if not in DNS. | no
d9912c
 `reverse` | Reverse DNS detection. | no
d9912c
-`ip_address` \| `ipaddress` | The host IP address. | no
d9912c
-`update_dns` | Update DNS entries. | no
d9912c
+`ip_address` \| `ipaddress` | The host IP address list. It can contain IPv4 and IPv6 addresses. No conflict check for IP addresses is done. | no
d9912c
+`update_dns` | For existing hosts: DNS SSHFP records are updated with `state` present and all DNS entries for a host removed with `state` absent. | no
d9912c
 
d9912c
 
d9912c
 Return Values
d9912c
diff --git a/playbooks/host/host-member-ipaddresses-absent.yml b/playbooks/host/host-member-ipaddresses-absent.yml
d9912c
new file mode 100644
d9912c
index 0000000..2466dbd
d9912c
--- /dev/null
d9912c
+++ b/playbooks/host/host-member-ipaddresses-absent.yml
d9912c
@@ -0,0 +1,17 @@
d9912c
+---
d9912c
+- name: Host member IP addresses absent
d9912c
+  hosts: ipaserver
d9912c
+  become: true
d9912c
+
d9912c
+  tasks:
d9912c
+  - name: Ensure host01.example.com IP addresses absent
d9912c
+    ipahost:
d9912c
+      ipaadmin_password: MyPassword123
d9912c
+      name: host01.example.com
d9912c
+      ip_address:
d9912c
+      - 192.168.0.123
d9912c
+      - fe80::20c:29ff:fe02:a1b3
d9912c
+      - 192.168.0.124
d9912c
+      - fe80::20c:29ff:fe02:a1b4
d9912c
+      action: member
d9912c
+      state: absent
d9912c
diff --git a/playbooks/host/host-member-ipaddresses-present.yml b/playbooks/host/host-member-ipaddresses-present.yml
d9912c
new file mode 100644
d9912c
index 0000000..f473993
d9912c
--- /dev/null
d9912c
+++ b/playbooks/host/host-member-ipaddresses-present.yml
d9912c
@@ -0,0 +1,16 @@
d9912c
+---
d9912c
+- name: Host member IP addresses present
d9912c
+  hosts: ipaserver
d9912c
+  become: true
d9912c
+
d9912c
+  tasks:
d9912c
+  - name: Ensure host01.example.com IP addresses present
d9912c
+    ipahost:
d9912c
+      ipaadmin_password: MyPassword123
d9912c
+      name: host01.example.com
d9912c
+      ip_address:
d9912c
+      - 192.168.0.123
d9912c
+      - fe80::20c:29ff:fe02:a1b3
d9912c
+      - 192.168.0.124
d9912c
+      - fe80::20c:29ff:fe02:a1b4
d9912c
+      action: member
d9912c
diff --git a/playbooks/host/host-present-with-several-ip-addresses.yml b/playbooks/host/host-present-with-several-ip-addresses.yml
d9912c
new file mode 100644
d9912c
index 0000000..4956562
d9912c
--- /dev/null
d9912c
+++ b/playbooks/host/host-present-with-several-ip-addresses.yml
d9912c
@@ -0,0 +1,24 @@
d9912c
+---
d9912c
+- name: Host present with several IP addresses
d9912c
+  hosts: ipaserver
d9912c
+  become: true
d9912c
+
d9912c
+  tasks:
d9912c
+  - name: Ensure host is present
d9912c
+    ipahost:
d9912c
+      ipaadmin_password: MyPassword123
d9912c
+      name: host01.example.com
d9912c
+      description: Example host
d9912c
+      ip_address:
d9912c
+      - 192.168.0.123
d9912c
+      - fe80::20c:29ff:fe02:a1b3
d9912c
+      - 192.168.0.124
d9912c
+      - fe80::20c:29ff:fe02:a1b4
d9912c
+      locality: Lab
d9912c
+      ns_host_location: Lab
d9912c
+      ns_os_version: CentOS 7
d9912c
+      ns_hardware_platform: Lenovo T61
d9912c
+      mac_address:
d9912c
+      - "08:00:27:E3:B1:2D"
d9912c
+      - "52:54:00:BD:97:1E"
d9912c
+      state: present
d9912c
diff --git a/plugins/module_utils/ansible_freeipa_module.py b/plugins/module_utils/ansible_freeipa_module.py
d9912c
index 9e97b88..6acdbef 100644
d9912c
--- a/plugins/module_utils/ansible_freeipa_module.py
d9912c
+++ b/plugins/module_utils/ansible_freeipa_module.py
d9912c
@@ -42,6 +42,7 @@
d9912c
     from ipalib.x509 import Encoding
d9912c
 except ImportError:
d9912c
     from cryptography.hazmat.primitives.serialization import Encoding
d9912c
+import socket
d9912c
 import base64
d9912c
 import six
d9912c
 
d9912c
@@ -285,3 +286,25 @@ def encode_certificate(cert):
d9912c
     if not six.PY2:
d9912c
         encoded = encoded.decode('ascii')
d9912c
     return encoded
d9912c
+
d9912c
+
d9912c
+def is_ipv4_addr(ipaddr):
d9912c
+    """
d9912c
+    Test if figen IP address is a valid IPv4 address
d9912c
+    """
d9912c
+    try:
d9912c
+        socket.inet_pton(socket.AF_INET, ipaddr)
d9912c
+    except socket.error:
d9912c
+        return False
d9912c
+    return True
d9912c
+
d9912c
+
d9912c
+def is_ipv6_addr(ipaddr):
d9912c
+    """
d9912c
+    Test if figen IP address is a valid IPv6 address
d9912c
+    """
d9912c
+    try:
d9912c
+        socket.inet_pton(socket.AF_INET6, ipaddr)
d9912c
+    except socket.error:
d9912c
+        return False
d9912c
+    return True
d9912c
diff --git a/plugins/modules/ipahost.py b/plugins/modules/ipahost.py
d9912c
index dba4181..a5fd482 100644
d9912c
--- a/plugins/modules/ipahost.py
d9912c
+++ b/plugins/modules/ipahost.py
d9912c
@@ -176,11 +176,16 @@
d9912c
         default: true
d9912c
         required: false
d9912c
       ip_address:
d9912c
-        description: The host IP address
d9912c
+        description:
d9912c
+          The host IP address list (IPv4 and IPv6). No IP address conflict
d9912c
+          check will be done.
d9912c
         aliases: ["ipaddress"]
d9912c
         required: false
d9912c
       update_dns:
d9912c
-        description: Update DNS entries
d9912c
+        description:
d9912c
+          Controls the update of the DNS SSHFP records for existing hosts and
d9912c
+          the removal of all DNS entries if a host gets removed with state
d9912c
+          absent.
d9912c
         required: false
d9912c
   description:
d9912c
     description: The host description
d9912c
@@ -306,11 +311,16 @@
d9912c
     default: true
d9912c
     required: false
d9912c
   ip_address:
d9912c
-    description: The host IP address
d9912c
+    description:
d9912c
+      The host IP address list (IPv4 and IPv6). No IP address conflict
d9912c
+      check will be done.
d9912c
     aliases: ["ipaddress"]
d9912c
     required: false
d9912c
   update_dns:
d9912c
-    description: Update DNS entries
d9912c
+    description:
d9912c
+      Controls the update of the DNS SSHFP records for existing hosts and
d9912c
+      the removal of all DNS entries if a host gets removed with state
d9912c
+      absent.
d9912c
     required: false
d9912c
   update_password:
d9912c
     description:
d9912c
@@ -398,7 +408,8 @@
d9912c
 from ansible.module_utils._text import to_text
d9912c
 from ansible.module_utils.ansible_freeipa_module import temp_kinit, \
d9912c
     temp_kdestroy, valid_creds, api_connect, api_command, compare_args_ipa, \
d9912c
-    module_params_get, gen_add_del_lists, encode_certificate, api_get_realm
d9912c
+    module_params_get, gen_add_del_lists, encode_certificate, api_get_realm, \
d9912c
+    is_ipv4_addr, is_ipv6_addr
d9912c
 import six
d9912c
 
d9912c
 
d9912c
@@ -428,6 +439,32 @@ def find_host(module, name):
d9912c
         return None
d9912c
 
d9912c
 
d9912c
+def find_dnsrecord(module, name):
d9912c
+    domain_name = name[name.find(".")+1:]
d9912c
+    host_name = name[:name.find(".")]
d9912c
+
d9912c
+    _args = {
d9912c
+        "all": True,
d9912c
+        "idnsname": to_text(host_name),
d9912c
+    }
d9912c
+
d9912c
+    _result = api_command(module, "dnsrecord_find", to_text(domain_name),
d9912c
+                          _args)
d9912c
+
d9912c
+    if len(_result["result"]) > 1:
d9912c
+        module.fail_json(
d9912c
+            msg="There is more than one host '%s'" % (name))
d9912c
+    elif len(_result["result"]) == 1:
d9912c
+        _res = _result["result"][0]
d9912c
+        certs = _res.get("usercertificate")
d9912c
+        if certs is not None:
d9912c
+            _res["usercertificate"] = [encode_certificate(cert) for
d9912c
+                                       cert in certs]
d9912c
+        return _res
d9912c
+    else:
d9912c
+        return None
d9912c
+
d9912c
+
d9912c
 def show_host(module, name):
d9912c
     _result = api_command(module, "host_show", to_text(name), {})
d9912c
     return _result["result"]
d9912c
@@ -470,16 +507,34 @@ def gen_args(description, locality, location, platform, os, password, random,
d9912c
         _args["ipakrboktoauthasdelegate"] = ok_to_auth_as_delegate
d9912c
     if force is not None:
d9912c
         _args["force"] = force
d9912c
-    if reverse is not None:
d9912c
-        _args["no_reverse"] = not reverse
d9912c
     if ip_address is not None:
d9912c
-        _args["ip_address"] = ip_address
d9912c
+        # IP addresses are handed extra, therefore it is needed to set
d9912c
+        # the force option here to make sure that host-add is able to
d9912c
+        # add a host without IP address.
d9912c
+        _args["force"] = True
d9912c
     if update_dns is not None:
d9912c
         _args["updatedns"] = update_dns
d9912c
 
d9912c
     return _args
d9912c
 
d9912c
 
d9912c
+def gen_dnsrecord_args(module, ip_address, reverse):
d9912c
+    _args = {}
d9912c
+    if reverse is not None:
d9912c
+        _args["a_extra_create_reverse"] = reverse
d9912c
+        _args["aaaa_extra_create_reverse"] = reverse
d9912c
+    if ip_address is not None:
d9912c
+        for ip in ip_address:
d9912c
+            if is_ipv4_addr(ip):
d9912c
+                _args.setdefault("arecord", []).append(ip)
d9912c
+            elif is_ipv6_addr(ip):
d9912c
+                _args.setdefault("aaaarecord", []).append(ip)
d9912c
+            else:
d9912c
+                module.fail_json(msg="'%s' is not a valid IP address." % ip)
d9912c
+
d9912c
+    return _args
d9912c
+
d9912c
+
d9912c
 def check_parameters(
d9912c
         module, state, action,
d9912c
         description, locality, location, platform, os, password, random,
d9912c
@@ -499,8 +554,7 @@ def check_parameters(
d9912c
                        "os", "password", "random", "mac_address", "sshpubkey",
d9912c
                        "userclass", "auth_ind", "requires_pre_auth",
d9912c
                        "ok_as_delegate", "ok_to_auth_as_delegate", "force",
d9912c
-                       "reverse", "ip_address", "update_dns",
d9912c
-                       "update_password"]
d9912c
+                       "reverse", "update_dns", "update_password"]
d9912c
             for x in invalid:
d9912c
                 if vars()[x] is not None:
d9912c
                     module.fail_json(
d9912c
@@ -512,7 +566,7 @@ def check_parameters(
d9912c
                    "password", "random", "mac_address", "sshpubkey",
d9912c
                    "userclass", "auth_ind", "requires_pre_auth",
d9912c
                    "ok_as_delegate", "ok_to_auth_as_delegate", "force",
d9912c
-                   "reverse", "ip_address", "update_password"]
d9912c
+                   "reverse", "update_password"]
d9912c
         for x in invalid:
d9912c
             if vars()[x] is not None:
d9912c
                 module.fail_json(
d9912c
@@ -549,9 +603,6 @@ def main():
d9912c
                       default=None, no_log=True),
d9912c
         random=dict(type="bool", aliases=["random_password"],
d9912c
                     default=None),
d9912c
-
d9912c
-
d9912c
-
d9912c
         certificate=dict(type="list", aliases=["usercertificate"],
d9912c
                          default=None),
d9912c
         managedby_host=dict(type="list",
d9912c
@@ -608,7 +659,7 @@ def main():
d9912c
                                     default=None),
d9912c
         force=dict(type='bool', default=None),
d9912c
         reverse=dict(type='bool', default=None),
d9912c
-        ip_address=dict(type="str", aliases=["ipaddress"],
d9912c
+        ip_address=dict(type="list", aliases=["ipaddress"],
d9912c
                         default=None),
d9912c
         update_dns=dict(type="bool", aliases=["updatedns"],
d9912c
                         default=None),
d9912c
@@ -820,6 +871,7 @@ def main():
d9912c
 
d9912c
             # Make sure host exists
d9912c
             res_find = find_host(ansible_module, name)
d9912c
+            res_find_dnsrecord = find_dnsrecord(ansible_module, name)
d9912c
 
d9912c
             # Create command
d9912c
             if state == "present":
d9912c
@@ -829,6 +881,8 @@ def main():
d9912c
                     random, mac_address, sshpubkey, userclass, auth_ind,
d9912c
                     requires_pre_auth, ok_as_delegate, ok_to_auth_as_delegate,
d9912c
                     force, reverse, ip_address, update_dns)
d9912c
+                dnsrecord_args = gen_dnsrecord_args(
d9912c
+                    ansible_module, ip_address, reverse)
d9912c
 
d9912c
                 if action == "host":
d9912c
                     # Found the host
d9912c
@@ -938,39 +992,20 @@ def main():
d9912c
                                 res_find.get(
d9912c
                                     "ipaallowedtoperform_read_keys_hostgroup"))
d9912c
 
d9912c
-                    else:
d9912c
-                        certificate_add = certificate or []
d9912c
-                        certificate_del = []
d9912c
-                        managedby_host_add = managedby_host or []
d9912c
-                        managedby_host_del = []
d9912c
-                        principal_add = principal or []
d9912c
-                        principal_del = []
d9912c
-                        allow_create_keytab_user_add = \
d9912c
-                            allow_create_keytab_user or []
d9912c
-                        allow_create_keytab_user_del = []
d9912c
-                        allow_create_keytab_group_add = \
d9912c
-                            allow_create_keytab_group or []
d9912c
-                        allow_create_keytab_group_del = []
d9912c
-                        allow_create_keytab_host_add = \
d9912c
-                            allow_create_keytab_host or []
d9912c
-                        allow_create_keytab_host_del = []
d9912c
-                        allow_create_keytab_hostgroup_add = \
d9912c
-                            allow_create_keytab_hostgroup or []
d9912c
-                        allow_create_keytab_hostgroup_del = []
d9912c
-                        allow_retrieve_keytab_user_add = \
d9912c
-                            allow_retrieve_keytab_user or []
d9912c
-                        allow_retrieve_keytab_user_del = []
d9912c
-                        allow_retrieve_keytab_group_add = \
d9912c
-                            allow_retrieve_keytab_group or []
d9912c
-                        allow_retrieve_keytab_group_del = []
d9912c
-                        allow_retrieve_keytab_host_add = \
d9912c
-                            allow_retrieve_keytab_host or []
d9912c
-                        allow_retrieve_keytab_host_del = []
d9912c
-                        allow_retrieve_keytab_hostgroup_add = \
d9912c
-                            allow_retrieve_keytab_hostgroup or []
d9912c
-                        allow_retrieve_keytab_hostgroup_del = []
d9912c
+                        # IP addresses are not really a member of hosts, but
d9912c
+                        # we will simply treat it as this to enable the
d9912c
+                        # addition and removal of IPv4 and IPv6 addresses in
d9912c
+                        # a simple way.
d9912c
+                        _dnsrec = res_find_dnsrecord or {}
d9912c
+                        dnsrecord_a_add, dnsrecord_a_del = gen_add_del_lists(
d9912c
+                            dnsrecord_args.get("arecord"),
d9912c
+                            _dnsrec.get("arecord"))
d9912c
+                        dnsrecord_aaaa_add, dnsrecord_aaaa_del = \
d9912c
+                            gen_add_del_lists(
d9912c
+                                dnsrecord_args.get("aaaarecord"),
d9912c
+                                _dnsrec.get("aaaarecord"))
d9912c
 
d9912c
-                else:
d9912c
+                if action != "host" or (action == "host" and res_find is None):
d9912c
                     certificate_add = certificate or []
d9912c
                     certificate_del = []
d9912c
                     managedby_host_add = managedby_host or []
d9912c
@@ -1001,6 +1036,10 @@ def main():
d9912c
                     allow_retrieve_keytab_hostgroup_add = \
d9912c
                         allow_retrieve_keytab_hostgroup or []
d9912c
                     allow_retrieve_keytab_hostgroup_del = []
d9912c
+                    dnsrecord_a_add = dnsrecord_args.get("arecord") or []
d9912c
+                    dnsrecord_a_del = []
d9912c
+                    dnsrecord_aaaa_add = dnsrecord_args.get("aaaarecord") or []
d9912c
+                    dnsrecord_aaaa_del = []
d9912c
 
d9912c
                 # Remove canonical principal from principal_del
d9912c
                 canonical_principal = "host/" + name + "@" + server_realm
d9912c
@@ -1135,6 +1174,36 @@ def main():
d9912c
                              "hostgroup": allow_retrieve_keytab_hostgroup_del,
d9912c
                          }])
d9912c
 
d9912c
+                if len(dnsrecord_a_add) > 0 or len(dnsrecord_aaaa_add) > 0:
d9912c
+                    domain_name = name[name.find(".")+1:]
d9912c
+                    host_name = name[:name.find(".")]
d9912c
+
d9912c
+                    commands.append([domain_name,
d9912c
+                                     "dnsrecord_add",
d9912c
+                                     {
d9912c
+                                         "idnsname": host_name,
d9912c
+                                         "arecord": dnsrecord_a_add,
d9912c
+                                         "a_extra_create_reverse": reverse,
d9912c
+                                         "aaaarecord": dnsrecord_aaaa_add,
d9912c
+                                         "aaaa_extra_create_reverse": reverse
d9912c
+                                     }])
d9912c
+
d9912c
+                if len(dnsrecord_a_del) > 0 or len(dnsrecord_aaaa_del) > 0:
d9912c
+                    domain_name = name[name.find(".")+1:]
d9912c
+                    host_name = name[:name.find(".")]
d9912c
+
d9912c
+                    # There seems to be an issue with dnsrecord_del (not
d9912c
+                    # for dnsrecord_add) if aaaarecord is an empty list.
d9912c
+                    # Therefore this is done differently here:
d9912c
+                    _args = {"idnsname": host_name}
d9912c
+                    if len(dnsrecord_a_del) > 0:
d9912c
+                        _args["arecord"] = dnsrecord_a_del
d9912c
+                    if len(dnsrecord_aaaa_del) > 0:
d9912c
+                        _args["aaaarecord"] = dnsrecord_aaaa_del
d9912c
+
d9912c
+                    commands.append([domain_name,
d9912c
+                                     "dnsrecord_del", _args])
d9912c
+
d9912c
             elif state == "absent":
d9912c
                 if action == "host":
d9912c
 
d9912c
@@ -1215,6 +1284,17 @@ def main():
d9912c
                                  "hostgroup": allow_retrieve_keytab_hostgroup,
d9912c
                              }])
d9912c
 
d9912c
+                    dnsrecord_args = gen_dnsrecord_args(ansible_module,
d9912c
+                                                        ip_address, reverse)
d9912c
+                    if "arecord" in dnsrecord_args or \
d9912c
+                       "aaaarecord" in dnsrecord_args:
d9912c
+                        domain_name = name[name.find(".")+1:]
d9912c
+                        host_name = name[:name.find(".")]
d9912c
+                        dnsrecord_args["idnsname"] = host_name
d9912c
+
d9912c
+                        commands.append([domain_name, "dnsrecord_del",
d9912c
+                                         dnsrecord_args])
d9912c
+
d9912c
             elif state == "disabled":
d9912c
                 if res_find is not None:
d9912c
                     commands.append([name, "host_disable", {}])
d9912c
@@ -1259,6 +1339,11 @@ def main():
d9912c
                 # Host is already disabled, ignore error
d9912c
                 if "This entry is already disabled" in msg:
d9912c
                     continue
d9912c
+
d9912c
+                # Ignore no modification error.
d9912c
+                if "no modifications to be performed" in msg:
d9912c
+                    continue
d9912c
+
d9912c
                 ansible_module.fail_json(msg="%s: %s: %s" % (command, name,
d9912c
                                                              msg))
d9912c
 
d9912c
diff --git a/tests/host/test_host_ipaddresses.yml b/tests/host/test_host_ipaddresses.yml
d9912c
new file mode 100644
d9912c
index 0000000..0a97dd5
d9912c
--- /dev/null
d9912c
+++ b/tests/host/test_host_ipaddresses.yml
d9912c
@@ -0,0 +1,312 @@
d9912c
+---
d9912c
+- name: Test host IP addresses
d9912c
+  hosts: ipaserver
d9912c
+  become: true
d9912c
+
d9912c
+  tasks:
d9912c
+  - name: Get Domain from server name
d9912c
+    set_fact:
d9912c
+      ipaserver_domain: "{{ groups.ipaserver[0].split('.')[1:] | join ('.') }}"
d9912c
+    when: ipaserver_domain is not defined
d9912c
+
d9912c
+  - name: Set host1_fqdn .. host6_fqdn
d9912c
+    set_fact:
d9912c
+      host1_fqdn: "{{ 'host1.' + ipaserver_domain }}"
d9912c
+      host2_fqdn: "{{ 'host2.' + ipaserver_domain }}"
d9912c
+      host3_fqdn: "{{ 'host3.' + ipaserver_domain }}"
d9912c
+
d9912c
+  - name: Get IPv4 address prefix from server node
d9912c
+    set_fact:
d9912c
+      ipv4_prefix: "{{ ansible_default_ipv4.address.split('.')[:-1] |
d9912c
+                       join('.') }}"
d9912c
+
d9912c
+  - name: Host absent
d9912c
+    ipahost:
d9912c
+      ipaadmin_password: MyPassword123
d9912c
+      name:
d9912c
+      - "{{ host1_fqdn }}"
d9912c
+      - "{{ host2_fqdn }}"
d9912c
+      - "{{ host3_fqdn }}"
d9912c
+      update_dns: yes
d9912c
+      state: absent
d9912c
+
d9912c
+  - name: Host "{{ host1_fqdn }}" present
d9912c
+    ipahost:
d9912c
+      ipaadmin_password: MyPassword123
d9912c
+      name: "{{ host1_fqdn }}"
d9912c
+      ip_address:
d9912c
+      - "{{ ipv4_prefix + '.201' }}"
d9912c
+      - fe80::20c:29ff:fe02:a1b2
d9912c
+      update_dns: yes
d9912c
+      reverse: no
d9912c
+    register: result
d9912c
+    failed_when: not result.changed
d9912c
+
d9912c
+  - name: Host "{{ host1_fqdn }}" present again
d9912c
+    ipahost:
d9912c
+      ipaadmin_password: MyPassword123
d9912c
+      name: "{{ host1_fqdn }}"
d9912c
+      ip_address:
d9912c
+      - "{{ ipv4_prefix + '.201' }}"
d9912c
+      - fe80::20c:29ff:fe02:a1b2
d9912c
+      update_dns: yes
d9912c
+      reverse: no
d9912c
+    register: result
d9912c
+    failed_when: result.changed
d9912c
+
d9912c
+  - name: Host "{{ host1_fqdn }}" present again with new IP address
d9912c
+    ipahost:
d9912c
+      ipaadmin_password: MyPassword123
d9912c
+      name: "{{ host1_fqdn }}"
d9912c
+      ip_address:
d9912c
+      - "{{ ipv4_prefix + '.211' }}"
d9912c
+      - fe80::20c:29ff:fe02:a1b3
d9912c
+      - "{{ ipv4_prefix + '.221' }}"
d9912c
+      - fe80::20c:29ff:fe02:a1b4
d9912c
+      update_dns: yes
d9912c
+      reverse: no
d9912c
+    register: result
d9912c
+    failed_when: not result.changed
d9912c
+
d9912c
+  - name: Host "{{ host1_fqdn }}" present again with new IP address again
d9912c
+    ipahost:
d9912c
+      ipaadmin_password: MyPassword123
d9912c
+      name: "{{ host1_fqdn }}"
d9912c
+      ip_address:
d9912c
+      - "{{ ipv4_prefix + '.211' }}"
d9912c
+      - fe80::20c:29ff:fe02:a1b3
d9912c
+      - "{{ ipv4_prefix + '.221' }}"
d9912c
+      - fe80::20c:29ff:fe02:a1b4
d9912c
+      update_dns: yes
d9912c
+      reverse: no
d9912c
+    register: result
d9912c
+    failed_when: result.changed
d9912c
+
d9912c
+  - name: Host "{{ host1_fqdn }}" member IPv4 address present
d9912c
+    ipahost:
d9912c
+      ipaadmin_password: MyPassword123
d9912c
+      name: "{{ host1_fqdn }}"
d9912c
+      ip_address: "{{ ipv4_prefix + '.201' }}"
d9912c
+      action: member
d9912c
+    register: result
d9912c
+    failed_when: not result.changed
d9912c
+
d9912c
+  - name: Host "{{ host1_fqdn }}" member IPv4 address present again
d9912c
+    ipahost:
d9912c
+      ipaadmin_password: MyPassword123
d9912c
+      name: "{{ host1_fqdn }}"
d9912c
+      ip_address: "{{ ipv4_prefix + '.201' }}"
d9912c
+      action: member
d9912c
+    register: result
d9912c
+    failed_when: result.changed
d9912c
+
d9912c
+  - name: Host "{{ host1_fqdn }}" member IPv4 address absent
d9912c
+    ipahost:
d9912c
+      ipaadmin_password: MyPassword123
d9912c
+      name: "{{ host1_fqdn }}"
d9912c
+      ip_address: "{{ ipv4_prefix + '.201' }}"
d9912c
+      action: member
d9912c
+      state: absent
d9912c
+    register: result
d9912c
+    failed_when: not result.changed
d9912c
+
d9912c
+  - name: Host "{{ host1_fqdn }}" member IPv4 address absent again
d9912c
+    ipahost:
d9912c
+      ipaadmin_password: MyPassword123
d9912c
+      name: "{{ host1_fqdn }}"
d9912c
+      ip_address: "{{ ipv4_prefix + '.201' }}"
d9912c
+      action: member
d9912c
+      state: absent
d9912c
+    register: result
d9912c
+    failed_when: result.changed
d9912c
+
d9912c
+  - name: Host "{{ host1_fqdn }}" member IPv6 address present
d9912c
+    ipahost:
d9912c
+      ipaadmin_password: MyPassword123
d9912c
+      name: "{{ host1_fqdn }}"
d9912c
+      ip_address: fe80::20c:29ff:fe02:a1b2
d9912c
+      action: member
d9912c
+    register: result
d9912c
+    failed_when: not result.changed
d9912c
+
d9912c
+  - name: Host "{{ host1_fqdn }}" member IPv6 address present again
d9912c
+    ipahost:
d9912c
+      ipaadmin_password: MyPassword123
d9912c
+      name: "{{ host1_fqdn }}"
d9912c
+      ip_address: fe80::20c:29ff:fe02:a1b2
d9912c
+      action: member
d9912c
+    register: result
d9912c
+    failed_when: result.changed
d9912c
+
d9912c
+  - name: Host "{{ host1_fqdn }}" member IPv6 address absent
d9912c
+    ipahost:
d9912c
+      ipaadmin_password: MyPassword123
d9912c
+      name: "{{ host1_fqdn }}"
d9912c
+      ip_address: fe80::20c:29ff:fe02:a1b2
d9912c
+      action: member
d9912c
+      state: absent
d9912c
+    register: result
d9912c
+    failed_when: not result.changed
d9912c
+
d9912c
+  - name: Host "{{ host1_fqdn }}" member IPv6 address absent again
d9912c
+    ipahost:
d9912c
+      ipaadmin_password: MyPassword123
d9912c
+      name: "{{ host1_fqdn }}"
d9912c
+      ip_address: fe80::20c:29ff:fe02:a1b2
d9912c
+      action: member
d9912c
+      state: absent
d9912c
+    register: result
d9912c
+
d9912c
+  - name: Host "{{ host1_fqdn }}" member all ip-addresses absent
d9912c
+    ipahost:
d9912c
+      ipaadmin_password: MyPassword123
d9912c
+      name: "{{ host1_fqdn }}"
d9912c
+      ip_address:
d9912c
+      - "{{ ipv4_prefix + '.211' }}"
d9912c
+      - fe80::20c:29ff:fe02:a1b3
d9912c
+      - "{{ ipv4_prefix + '.221' }}"
d9912c
+      - fe80::20c:29ff:fe02:a1b4
d9912c
+      action: member
d9912c
+      state: absent
d9912c
+    register: result
d9912c
+    failed_when: not result.changed
d9912c
+
d9912c
+  - name: Host "{{ host1_fqdn }}" all member ip-addresses absent again
d9912c
+    ipahost:
d9912c
+      ipaadmin_password: MyPassword123
d9912c
+      name: "{{ host1_fqdn }}"
d9912c
+      ip_address:
d9912c
+      - "{{ ipv4_prefix + '.211' }}"
d9912c
+      - fe80::20c:29ff:fe02:a1b3
d9912c
+      - "{{ ipv4_prefix + '.221' }}"
d9912c
+      - fe80::20c:29ff:fe02:a1b4
d9912c
+      action: member
d9912c
+      state: absent
d9912c
+    register: result
d9912c
+    failed_when: result.changed
d9912c
+
d9912c
+  - name: Hosts "{{ host1_fqdn }}" and "{{ host2_fqdn }}" present with same IP addresses
d9912c
+    ipahost:
d9912c
+      ipaadmin_password: MyPassword123
d9912c
+      hosts:
d9912c
+      - name: "{{ host1_fqdn }}"
d9912c
+        ip_address:
d9912c
+        - "{{ ipv4_prefix + '.211' }}"
d9912c
+        - fe80::20c:29ff:fe02:a1b3
d9912c
+        - "{{ ipv4_prefix + '.221' }}"
d9912c
+        - fe80::20c:29ff:fe02:a1b4
d9912c
+      - name: "{{ host2_fqdn }}"
d9912c
+        ip_address:
d9912c
+        - "{{ ipv4_prefix + '.211' }}"
d9912c
+        - fe80::20c:29ff:fe02:a1b3
d9912c
+        - "{{ ipv4_prefix + '.221' }}"
d9912c
+        - fe80::20c:29ff:fe02:a1b4
d9912c
+    register: result
d9912c
+    failed_when: not result.changed
d9912c
+
d9912c
+  - name: Hosts "{{ host1_fqdn }}" and "{{ host2_fqdn }}" present with same IP addresses again
d9912c
+    ipahost:
d9912c
+      ipaadmin_password: MyPassword123
d9912c
+      hosts:
d9912c
+      - name: "{{ host1_fqdn }}"
d9912c
+        ip_address:
d9912c
+        - "{{ ipv4_prefix + '.211' }}"
d9912c
+        - fe80::20c:29ff:fe02:a1b3
d9912c
+        - "{{ ipv4_prefix + '.221' }}"
d9912c
+        - fe80::20c:29ff:fe02:a1b4
d9912c
+      - name: "{{ host2_fqdn }}"
d9912c
+        ip_address:
d9912c
+        - "{{ ipv4_prefix + '.211' }}"
d9912c
+        - fe80::20c:29ff:fe02:a1b3
d9912c
+        - "{{ ipv4_prefix + '.221' }}"
d9912c
+        - fe80::20c:29ff:fe02:a1b4
d9912c
+    register: result
d9912c
+    failed_when: result.changed
d9912c
+
d9912c
+  - name: Hosts "{{ host3_fqdn }}" present with same IP addresses
d9912c
+    ipahost:
d9912c
+      ipaadmin_password: MyPassword123
d9912c
+      hosts:
d9912c
+      - name: "{{ host3_fqdn }}"
d9912c
+        ip_address:
d9912c
+        - "{{ ipv4_prefix + '.211' }}"
d9912c
+        - fe80::20c:29ff:fe02:a1b3
d9912c
+        - "{{ ipv4_prefix + '.221' }}"
d9912c
+        - fe80::20c:29ff:fe02:a1b4
d9912c
+    register: result
d9912c
+    failed_when: not result.changed
d9912c
+
d9912c
+  - name: Hosts "{{ host3_fqdn }}" present with same IP addresses again
d9912c
+    ipahost:
d9912c
+      ipaadmin_password: MyPassword123
d9912c
+      hosts:
d9912c
+      - name: "{{ host3_fqdn }}"
d9912c
+        ip_address:
d9912c
+        - "{{ ipv4_prefix + '.211' }}"
d9912c
+        - fe80::20c:29ff:fe02:a1b3
d9912c
+        - "{{ ipv4_prefix + '.221' }}"
d9912c
+        - fe80::20c:29ff:fe02:a1b4
d9912c
+    register: result
d9912c
+    failed_when: result.changed
d9912c
+
d9912c
+  - name: Host "{{ host3_fqdn }}" present with differnt IP addresses
d9912c
+    ipahost:
d9912c
+      ipaadmin_password: MyPassword123
d9912c
+      hosts:
d9912c
+      - name: "{{ host3_fqdn }}"
d9912c
+        ip_address:
d9912c
+        - "{{ ipv4_prefix + '.111' }}"
d9912c
+        - fe80::20c:29ff:fe02:a1b1
d9912c
+        - "{{ ipv4_prefix + '.121' }}"
d9912c
+        - fe80::20c:29ff:fe02:a1b2
d9912c
+    register: result
d9912c
+    failed_when: not result.changed
d9912c
+
d9912c
+  - name: Host "{{ host3_fqdn }}" present with different IP addresses again
d9912c
+    ipahost:
d9912c
+      ipaadmin_password: MyPassword123
d9912c
+      hosts:
d9912c
+      - name: "{{ host3_fqdn }}"
d9912c
+        ip_address:
d9912c
+        - "{{ ipv4_prefix + '.111' }}"
d9912c
+        - fe80::20c:29ff:fe02:a1b1
d9912c
+        - "{{ ipv4_prefix + '.121' }}"
d9912c
+        - fe80::20c:29ff:fe02:a1b2
d9912c
+    register: result
d9912c
+    failed_when: result.changed
d9912c
+
d9912c
+  - name: Host "{{ host3_fqdn }}" present with old IP addresses
d9912c
+    ipahost:
d9912c
+      ipaadmin_password: MyPassword123
d9912c
+      hosts:
d9912c
+      - name: "{{ host3_fqdn }}"
d9912c
+        ip_address:
d9912c
+        - "{{ ipv4_prefix + '.211' }}"
d9912c
+        - fe80::20c:29ff:fe02:a1b3
d9912c
+        - "{{ ipv4_prefix + '.221' }}"
d9912c
+        - fe80::20c:29ff:fe02:a1b4
d9912c
+    register: result
d9912c
+    failed_when: not result.changed
d9912c
+
d9912c
+  - name: Host "{{ host3_fqdn }}" present with old IP addresses again
d9912c
+    ipahost:
d9912c
+      ipaadmin_password: MyPassword123
d9912c
+      hosts:
d9912c
+      - name: "{{ host3_fqdn }}"
d9912c
+        ip_address:
d9912c
+        - "{{ ipv4_prefix + '.211' }}"
d9912c
+        - fe80::20c:29ff:fe02:a1b3
d9912c
+        - "{{ ipv4_prefix + '.221' }}"
d9912c
+        - fe80::20c:29ff:fe02:a1b4
d9912c
+    register: result
d9912c
+    failed_when: result.changed
d9912c
+
d9912c
+  - name: Host absent
d9912c
+    ipahost:
d9912c
+      ipaadmin_password: MyPassword123
d9912c
+      name:
d9912c
+      - "{{ host1_fqdn }}"
d9912c
+      - "{{ host2_fqdn }}"
d9912c
+      - "{{ host3_fqdn }}"
d9912c
+      update_dns: yes
d9912c
+      state: absent
d9912c
From 8f32cb04c1e161e1e3217f10413685a2cc9bf492 Mon Sep 17 00:00:00 2001
d9912c
From: Thomas Woerner <twoerner@redhat.com>
d9912c
Date: Thu, 13 Feb 2020 14:10:38 +0100
d9912c
Subject: [PATCH] tests/host/test_host: Fix use of wrong host in the host5 test
d9912c
d9912c
host1 was used instead of host5 in the repeated host5 test. This lead to an
d9912c
error with the new IP address handling in ipahost. It was correctly
d9912c
reporting a change for host1 which resulted in a failed test.
d9912c
---
d9912c
 tests/host/test_host.yml | 2 +-
d9912c
 1 file changed, 1 insertion(+), 1 deletion(-)
d9912c
d9912c
diff --git a/tests/host/test_host.yml b/tests/host/test_host.yml
d9912c
index 1a555a1..f3ec11d 100644
d9912c
--- a/tests/host/test_host.yml
d9912c
+++ b/tests/host/test_host.yml
d9912c
@@ -129,7 +129,7 @@
d9912c
   - name: Host "{{ host5_fqdn }}" present again
d9912c
     ipahost:
d9912c
       ipaadmin_password: MyPassword123
d9912c
-      name: "{{ host1_fqdn }}"
d9912c
+      name: "{{ host5_fqdn }}"
d9912c
       ip_address: "{{ ipv4_prefix + '.205' }}"
d9912c
       update_dns: yes
d9912c
       reverse: no