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

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