Repository: ansible/ansible

Issue:
os_recordset module: Unable to have both A and AAAA records
<!---
Verify first that your issue/request is not already reported on GitHub.
Also test if the latest release, and master branch are affected too.
-->

##### ISSUE TYPE
<!--- Pick one below and delete the rest: -->
 - Bug Report

##### COMPONENT NAME
<!--- Name of the module/plugin/task/feature -->
modules-extra / cloud / openstack / os_recordset

##### ANSIBLE VERSION
<!--- Paste verbatim output from “ansible --version” between quotes below -->
```
$ ansible --version
ansible 2.2.0.0
  config file = /home/ubuntu/work/playbooks/ansible.cfg
  configured module search path = Default w/o overrides

```

##### CONFIGURATION
<!---
Mention any settings you have changed/added/removed in ansible.cfg
(or using the ANSIBLE_* environment variables).
-->
```
[defaults]
inventory=inventory/openstack.py

[ssh_connection]
pipelining=False
```

##### OS / ENVIRONMENT
<!---
Mention the OS you are running Ansible from, and the OS you are
managing, or say “N/A” for anything that is not platform-specific.
-->
Ubuntu 16.04.  The task uses delegate_to: localhost

##### SUMMARY
<!--- Explain the problem briefly -->
It seems the os_recordset module is unable to create domain records with both A and  AAAA records if they both have the same name. This isn't uncommon in IPv4 and IPv6 deployments.  I should be able to create both A and AAAA records for the same name.

##### STEPS TO REPRODUCE
<!---
For bugs, show exactly how to reproduce the problem, using a minimal test-case.
For new features, show how the feature would be used.
-->

<!--- Paste example playbooks or commands between quotes below -->
```yaml
- hosts: testhost
  tasks:
    - name: Setting up testhost IPv4
      delegate_to: localhost
      os_recordset:
        cloud: dev
        state: present
        zone: "example.com."
        name: "www"
        recordset_type: A
        records: "{{ openstack.accessIPv4 }}"
        ttl: 3600
    - name: Setting up testhost IPv6
      delegate_to: localhost
      os_recordset:
        cloud: dev
        state: present
        zone: "example.com."
        name: "www"
        recordset_type: AAAA
        records: "{{ openstack.accessIPv6 }}"
        ttl: 3600

```

<!--- You can also paste gist.github.com links for larger files -->

##### EXPECTED RESULTS
<!--- What did you expect to happen when running the steps above? -->
I would expect both A and AAAA records to be created without error.

##### ACTUAL RESULTS
<!--- What actually happened? If possible run with extra verbosity (-vvvv) -->
Creating A/AAAA record and both an A and AAAA records already exist in OpenStack:
```
TASK [Setting up testhost IPv4] ************************************************
fatal: [www.example.com -> localhost]: FAILED! => {"changed": false, "failed": true, "msg": "Unable to create recordset www (Inner Exception: Duplicate RecordSet)"}
```
Creating A record and only an AAAA record already exists in OpenStack:
```
TASK [Setting up testhost IPv4] ************************************************
fatal: [www.example.com -> localhost]: FAILED! => {"changed": false, "failed": true, "msg": "Error updating recordset www.example.com. (Inner Exception: u'10.0.0.1' is not a 'ipv6')"}
```
Verbose on: 
```
fatal: [www.example.com -> localhost]: FAILED! => {
    "changed": false, 
    "failed": true, 
    "invocation": {
        "module_args": {
            "api_timeout": null, 
            "auth": null, 
            "auth_type": null, 
            "availability_zone": null, 
            "cacert": null, 
            "cert": null, 
            "cloud": "dev", 
            "description": null, 
            "endpoint_type": "public", 
            "key": null, 
            "name": "www", 
            "records": [
                "10.0.0.1"
            ], 
            "recordset_type": "A", 
            "region_name": null, 
            "state": "present", 
            "timeout": 180, 
            "ttl": 3600, 
            "verify": true, 
            "wait": true, 
            "zone": "example.com."
        }, 
        "module_name": "os_recordset"
    }, 
    "msg": "Error updating recordset www.example.com. (Inner Exception: u'10.0.0.1' is not a 'ipv6')"
}

```
Creating an AAAA record and an A record already exists in OpenStack
```
    "msg": "Error updating recordset www.example.com. (Inner Exception: u'<ipv6 address removed>' is not a 'ipv4')"

```




Discussion:
From the feel of the bug, if an A record already exists and I go to add an AAAA record using Ansible it tries to update the existing A record with an IPv6 address which will obviously fail (and vice versa).
cc @emonty, @shrews, @juliakreger, @j2sol, @rcarrillocruz, @thingee
[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: notify --->
So it looks like the module was never designed for multiple records with the same name..  I imagine it will fail if MX, TXT, etc.  also exist for a single named record as seen here:
https://github.com/ansible/ansible/blame/devel/lib/ansible/modules/cloud/openstack/os_recordset.py#L188

It explodes if an A + AAAA record exist with the `(Inner Exception: Duplicate RecordSet)` error above.

After it pulls back `recordset` with `get_recordset`, it doesn't actually check the `recordset['type']` value to see if it matches with what's being  returned.. Which explains the other errors above like trying to update an AAAA record type with an IPv4 address or A record with IPv6 address.



Resolving patch:
diff --git a/lib/ansible/modules/cloud/openstack/os_recordset.py b/lib/ansible/modules/cloud/openstack/os_recordset.py
--- a/lib/ansible/modules/cloud/openstack/os_recordset.py
+++ b/lib/ansible/modules/cloud/openstack/os_recordset.py
@@ -185,11 +185,22 @@ def main():
 
     try:
         cloud = shade.openstack_cloud(**module.params)
-        recordset = cloud.get_recordset(zone, name + '.' + zone)
+        recordset_type = module.params.get('recordset_type')
+        recordset_filter = { 'type': recordset_type  }
 
+        recordsets = cloud.search_recordsets(zone, name_or_id=name + '.' + zone, filters=recordset_filter)
+
+        if len(recordsets) == 1:
+            recordset = recordsets[0]
+            try:
+                recordset_id = recordset['id']
+            except KeyError as e:
+                module.fail_json(msg=str(e))
+        else:
+            # recordsets is filtered by type and should never be more than 1 return
+            recordset = None
 
         if state == 'present':
-            recordset_type = module.params.get('recordset_type')
             records = module.params.get('records')
             description = module.params.get('description')
             ttl = module.params.get('ttl')
@@ -215,10 +226,11 @@ def main():
                                                zone, pre_update_recordset)
                 if changed:
                     zone = cloud.update_recordset(
-                        zone, name + '.' + zone,
+                        zone, recordset_id,
                         records=records,
                         description=description,
                         ttl=ttl)
+
             module.exit_json(changed=changed, recordset=recordset)
 
         elif state == 'absent':
@@ -231,7 +243,7 @@ def main():
             if recordset is None:
                 changed=False
             else:
-                cloud.delete_recordset(zone, name + '.' + zone)
+                cloud.delete_recordset(zone, recordset_id)
                 changed=True
             module.exit_json(changed=changed)
 


Test patch: