Blob Blame History Raw
From 167c76311da72c2bfabf4b2bce9e128c11d519d0 Mon Sep 17 00:00:00 2001
From: Thomas Woerner <twoerner@redhat.com>
Date: Wed, 12 Feb 2020 16:54:13 +0100
Subject: [PATCH] ipahost: Add support for several IP addresses and also to
 change them

ipahost was so far ignoring IP addresses when the host already existed.
This happened because host_mod is not providing functionality to do this.
Now ipaddress is a list and it is possible to ensure a host with several
IP addresses (these can be IPv4 and IPv6). Also it is possible to ensure
presence and absence of IP addresses for an exising host using action
member.

There are no IP address conclict checks as this would lead into issues with
updating an existing host that already is using a duplicate IP address for
example for round-robin (RR). Also this might lead into issues with ensuring
a new host with several IP addresses in this case. Also to ensure a list of
hosts with changing the IP address of one host to another in the list would
result in issues here.

New example playbooks have been added:

    playbooks/host/host-present-with-several-ip-addresses.yml
    playbooks/host/host-member-ipaddresses-absent.yml
    playbooks/host/host-member-ipaddresses-present.yml

A new test has been added for verification:

    tests/host/test_host_ipaddresses.yml

Fixes: https://bugzilla.redhat.com/show_bug.cgi?id=1783976
       https://bugzilla.redhat.com/show_bug.cgi?id=1783979
---
 README-host.md                                |  79 ++++-
 .../host/host-member-ipaddresses-absent.yml   |  17 +
 .../host/host-member-ipaddresses-present.yml  |  16 +
 ...host-present-with-several-ip-addresses.yml |  24 ++
 .../module_utils/ansible_freeipa_module.py    |  23 ++
 plugins/modules/ipahost.py                    | 179 +++++++---
 tests/host/test_host_ipaddresses.yml          | 312 ++++++++++++++++++
 7 files changed, 600 insertions(+), 50 deletions(-)
 create mode 100644 playbooks/host/host-member-ipaddresses-absent.yml
 create mode 100644 playbooks/host/host-member-ipaddresses-present.yml
 create mode 100644 playbooks/host/host-present-with-several-ip-addresses.yml
 create mode 100644 tests/host/test_host_ipaddresses.yml

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

host1 was used instead of host5 in the repeated host5 test. This lead to an
error with the new IP address handling in ipahost. It was correctly
reporting a change for host1 which resulted in a failed test.
---
 tests/host/test_host.yml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/tests/host/test_host.yml b/tests/host/test_host.yml
index 1a555a1..f3ec11d 100644
--- a/tests/host/test_host.yml
+++ b/tests/host/test_host.yml
@@ -129,7 +129,7 @@
   - name: Host "{{ host5_fqdn }}" present again
     ipahost:
       ipaadmin_password: MyPassword123
-      name: "{{ host1_fqdn }}"
+      name: "{{ host5_fqdn }}"
       ip_address: "{{ ipv4_prefix + '.205' }}"
       update_dns: yes
       reverse: no