From 7437d6fdc4989e69f0381b5c8e1a08dd01fc48c3 Mon Sep 17 00:00:00 2001 From: David Passante Date: Sun, 22 Apr 2018 20:13:01 +0200 Subject: [PATCH] cs_ip_address: add a "tags" parameter to ensure idempotency (#39016) * cs_ip_address: add a "tags" parameter to manage idempotency * cs_ip_address: add integration tests --- .../modules/cloud/cloudstack/cs_ip_address.py | 64 +++++- .../integration/targets/cs_ip_address/aliases | 2 + .../targets/cs_ip_address/meta/main.yml | 3 + .../targets/cs_ip_address/tasks/main.yml | 212 ++++++++++++++++++ 4 files changed, 275 insertions(+), 6 deletions(-) create mode 100644 test/integration/targets/cs_ip_address/aliases create mode 100644 test/integration/targets/cs_ip_address/meta/main.yml create mode 100644 test/integration/targets/cs_ip_address/tasks/main.yml diff --git a/lib/ansible/modules/cloud/cloudstack/cs_ip_address.py b/lib/ansible/modules/cloud/cloudstack/cs_ip_address.py index 496e639b241..398fd83050a 100644 --- a/lib/ansible/modules/cloud/cloudstack/cs_ip_address.py +++ b/lib/ansible/modules/cloud/cloudstack/cs_ip_address.py @@ -17,7 +17,8 @@ short_description: Manages public IP address associations on Apache CloudStack b description: - Acquires and associates a public IP to an account or project. - Due to API limitations this is not an idempotent call, so be sure to only - conditionally call this when C(state=present) + conditionally call this when C(state=present). + - Tagging the IP address can also make the call idempotent. version_added: '2.0' author: - "Darren Worrall (@dazworrall)" @@ -26,7 +27,7 @@ options: ip_address: description: - Public IP address. - - Required if C(state=absent) + - Required if C(state=absent) and C(tags) is not set domain: description: - Domain the IP address is related to. @@ -52,6 +53,13 @@ options: - State of the IP address. default: present choices: [ present, absent ] + tags: + description: + - List of tags. Tags are a list of dictionaries having keys C(key) and C(value). + - Tags can be used as an unique identifier for the IP Addresses. + - In this case, at least one of them must be unique to ensure idempontency. + aliases: [ 'tag' ] + version_added: "2.6" poll_async: description: - Poll async jobs until job has finished. @@ -73,6 +81,23 @@ EXAMPLES = ''' module: cs_ip_address ip_address: 1.2.3.4 state: absent + +- name: Associate an IP address with tags + local_action: + module: cs_ip_address + network: My Network + tags: + - key: myCustomID + - value: 5510c31a-416e-11e8-9013-02000a6b00bf + register: ip_address + +- name: Disassociate an IP address with tags + local_action: + module: cs_ip_address + state: absent + tags: + - key: myCustomID + - value: 5510c31a-416e-11e8-9013-02000a6b00bf ''' RETURN = ''' @@ -107,6 +132,12 @@ domain: returned: success type: string sample: example domain +tags: + description: List of resource tags associated with the IP address. + returned: success + type: dict + sample: '[ { "key": "myCustomID", "value": "5510c31a-416e-11e8-9013-02000a6b00bf" } ]' + version_added: "2.6" ''' from ansible.module_utils.basic import AnsibleModule @@ -138,10 +169,27 @@ class AnsibleCloudStackIPAddress(AnsibleCloudStack): ip_addresses = self.cs.listPublicIpAddresses(**args) if ip_addresses: - self.ip_address = ip_addresses['publicipaddress'][0] + tags = self.module.params.get('tags') + for ip_addr in ip_addresses['publicipaddress']: + if ip_addr['ipaddress'] == args['ipaddress'] != '': + self.ip_address = ip_addresses['publicipaddress'][0] + elif tags: + if sorted([tag for tag in tags if tag in ip_addr['tags']]) == sorted(tags): + self.ip_address = ip_addr return self._get_by_key(key, self.ip_address) - def associate_ip_address(self): + def present_ip_address(self): + ip_address = self.get_ip_address() + + if not ip_address: + ip_address = self.associate_ip_address(ip_address) + + if ip_address: + ip_address = self.ensure_tags(resource=ip_address, resource_type='publicipaddress') + + return ip_address + + def associate_ip_address(self, ip_address): self.result['changed'] = True args = { 'account': self.get_account(key='name'), @@ -169,6 +217,9 @@ class AnsibleCloudStackIPAddress(AnsibleCloudStack): self.result['changed'] = True if not self.module.check_mode: + self.module.params['tags'] = [] + ip_address = self.ensure_tags(resource=ip_address, resource_type='publicipaddress') + res = self.cs.disassociateIpAddress(id=ip_address['id']) poll_async = self.module.params.get('poll_async') @@ -188,6 +239,7 @@ def main(): domain=dict(), account=dict(), project=dict(), + tags=dict(type='list', aliases=['tag']), poll_async=dict(type='bool', default=True), )) @@ -195,7 +247,7 @@ def main(): argument_spec=argument_spec, required_together=cs_required_together(), required_if=[ - ('state', 'absent', ['ip_address']), + ('state', 'absent', ['ip_address', 'tags'], True), ], supports_check_mode=True ) @@ -206,7 +258,7 @@ def main(): if state in ['absent']: ip_address = acs_ip_address.disassociate_ip_address() else: - ip_address = acs_ip_address.associate_ip_address() + ip_address = acs_ip_address.present_ip_address() result = acs_ip_address.get_result(ip_address) module.exit_json(**result) diff --git a/test/integration/targets/cs_ip_address/aliases b/test/integration/targets/cs_ip_address/aliases new file mode 100644 index 00000000000..ee8454c6d12 --- /dev/null +++ b/test/integration/targets/cs_ip_address/aliases @@ -0,0 +1,2 @@ +cloud/cs +posix/ci/cloud/group1/cs diff --git a/test/integration/targets/cs_ip_address/meta/main.yml b/test/integration/targets/cs_ip_address/meta/main.yml new file mode 100644 index 00000000000..e9a5b9eeaef --- /dev/null +++ b/test/integration/targets/cs_ip_address/meta/main.yml @@ -0,0 +1,3 @@ +--- +dependencies: + - cs_common diff --git a/test/integration/targets/cs_ip_address/tasks/main.yml b/test/integration/targets/cs_ip_address/tasks/main.yml new file mode 100644 index 00000000000..f6c707e51c2 --- /dev/null +++ b/test/integration/targets/cs_ip_address/tasks/main.yml @@ -0,0 +1,212 @@ +--- +- name: setup ensure the test network is absent + cs_network: + name: ipaddr_test_network + state: absent + zone: "{{ cs_common_zone_adv }}" + +- name: setup create the test network + cs_network: + name: ipaddr_test_network + network_offering: DefaultIsolatedNetworkOfferingWithSourceNatService + state: present + zone: "{{ cs_common_zone_adv }}" + register: base_network +- name: setup verify create the test network + assert: + that: + - base_network is successful + +- name: setup clean ip_address with tags + cs_ip_address: + state: absent + tags: + - key: unique_id + value: "adacd65e-7868-5ebf-9f8b-e6e0ea779861" + network: ipaddr_test_network + zone: "{{ cs_common_zone_adv }}" + +- name: setup associate ip_address for SNAT + cs_ip_address: + network: ipaddr_test_network + zone: "{{ cs_common_zone_adv }}" + register: ip_address_snat + +- name: test associate ip_address in check mode + cs_ip_address: + network: ipaddr_test_network + zone: "{{ cs_common_zone_adv }}" + check_mode: true + register: ip_address +- name: verify test associate ip_address in check mode + assert: + that: + - ip_address is successful + - ip_address is changed + +- name: test associate ip_address + cs_ip_address: + network: ipaddr_test_network + zone: "{{ cs_common_zone_adv }}" + register: ip_address +- name: verify test associate ip_address + assert: + that: + - ip_address is successful + - ip_address is changed + - ip_address.ip_address is defined + +- name: test associate ip_address with tags in check mode + cs_ip_address: + network: ipaddr_test_network + tags: + - key: unique_id + value: "adacd65e-7868-5ebf-9f8b-e6e0ea779861" + zone: "{{ cs_common_zone_adv }}" + register: ip_address_tag + check_mode: true +- name: verify test associate ip_address with tags in check mode + assert: + that: + - ip_address_tag is successful + - ip_address_tag is changed + +- name: test associate ip_address with tags + cs_ip_address: + network: ipaddr_test_network + tags: + - key: unique_id + value: "adacd65e-7868-5ebf-9f8b-e6e0ea779861" + zone: "{{ cs_common_zone_adv }}" + register: ip_address_tag +- name: verify test associate ip_address with tags + assert: + that: + - ip_address_tag is successful + - ip_address_tag is changed + - ip_address_tag.ip_address is defined + - ip_address_tag.tags.0.key == "unique_id" + - ip_address_tag.tags.0.value == "adacd65e-7868-5ebf-9f8b-e6e0ea779861" + +- name: test associate ip_address with tags idempotence + cs_ip_address: + network: ipaddr_test_network + tags: + - key: unique_id + value: "adacd65e-7868-5ebf-9f8b-e6e0ea779861" + zone: "{{ cs_common_zone_adv }}" + register: ip_address_tag +- name: verify test associate ip_address with tags idempotence + assert: + that: + - ip_address_tag is successful + - ip_address_tag is not changed + - ip_address_tag.ip_address is defined + - ip_address_tag.state == "Allocated" + - ip_address_tag.tags.0.key == "unique_id" + - ip_address_tag.tags.0.value == "adacd65e-7868-5ebf-9f8b-e6e0ea779861" + +- name: test disassiociate ip_address with missing param ip_address + cs_ip_address: + state: absent + network: ipaddr_test_network + zone: "{{ cs_common_zone_adv }}" + ignore_errors: true + register: ip_address_err +- name: verify test disassiociate ip_address with missing param ip_address + assert: + that: + - ip_address_err is failed + - 'ip_address_err.msg == "state is absent but any of the following are missing: ip_address, tags"' + +- name: test disassociate ip_address in check mode + cs_ip_address: + state: absent + ip_address: "{{ ip_address.ip_address }}" + network: ipaddr_test_network + zone: "{{ cs_common_zone_adv }}" + check_mode: true + register: ip_address +- name: verify test disassociate ip_address in check mode + assert: + that: + - ip_address is successful + - ip_address is changed + +- name: test disassociate ip_address + cs_ip_address: + state: absent + ip_address: "{{ ip_address.ip_address }}" + network: ipaddr_test_network + zone: "{{ cs_common_zone_adv }}" + register: ip_address +- name: verify test disassociate ip_address + assert: + that: + - ip_address is successful + - ip_address is changed + +- name: test disassociate ip_address idempotence + cs_ip_address: + state: absent + ip_address: "{{ ip_address.ip_address }}" + network: ipaddr_test_network + zone: "{{ cs_common_zone_adv }}" + register: ip_address +- name: verify test disassociate ip_address idempotence + assert: + that: + - ip_address is successful + - ip_address is not changed + +- name: test disassociate ip_address with tags with check mode + cs_ip_address: + state: absent + tags: + - key: unique_id + value: "adacd65e-7868-5ebf-9f8b-e6e0ea779861" + network: ipaddr_test_network + zone: "{{ cs_common_zone_adv }}" + check_mode: true + register: ip_address +- name: verify test disassociate ip_address with tags in check mode + assert: + that: + - ip_address is successful + - ip_address is changed + +- name: test disassociate ip_address with tags + cs_ip_address: + state: absent + tags: + - key: unique_id + value: "adacd65e-7868-5ebf-9f8b-e6e0ea779861" + network: ipaddr_test_network + zone: "{{ cs_common_zone_adv }}" + register: ip_address +- name: verify test disassociate ip_address with tags + assert: + that: + - ip_address is successful + - ip_address is changed + +- name: test disassociate ip_address with tags idempotence + cs_ip_address: + state: absent + tags: + - key: unique_id + value: "adacd65e-7868-5ebf-9f8b-e6e0ea779861" + network: ipaddr_test_network + zone: "{{ cs_common_zone_adv }}" + register: ip_address +- name: verify test disassociate ip_address with tags idempotence + assert: + that: + - ip_address is successful + - ip_address is not changed + +- name: clean the test network + cs_network: + name: ipaddr_test_network + state: absent + zone: "{{ cs_common_zone_adv }}"