diff --git a/lib/ansible/module_utils/cloudstack.py b/lib/ansible/module_utils/cloudstack.py index 82d5356533a..f46f35374cb 100644 --- a/lib/ansible/module_utils/cloudstack.py +++ b/lib/ansible/module_utils/cloudstack.py @@ -10,7 +10,7 @@ import os import sys import time -from ansible.module_utils._text import to_text +from ansible.module_utils._text import to_text, to_native try: from cs import CloudStack, CloudStackException, read_config @@ -146,7 +146,7 @@ class AnsibleCloudStack: value = self.module.params.get(fallback_key) return value - def has_changed(self, want_dict, current_dict, only_keys=None): + def has_changed(self, want_dict, current_dict, only_keys=None, skip_diff_for_keys=None): result = False for key, value in want_dict.items(): @@ -160,6 +160,7 @@ class AnsibleCloudStack: if key in current_dict: if isinstance(value, (int, float, long, complex)): + # ensure we compare the same type if isinstance(value, int): current_dict[key] = int(current_dict[key]) @@ -171,8 +172,9 @@ class AnsibleCloudStack: current_dict[key] = complex(current_dict[key]) if value != current_dict[key]: - self.result['diff']['before'][key] = current_dict[key] - self.result['diff']['after'][key] = value + if skip_diff_for_keys and key not in skip_diff_for_keys: + self.result['diff']['before'][key] = current_dict[key] + self.result['diff']['after'][key] = value result = True else: before_value = to_text(current_dict[key]) @@ -180,18 +182,21 @@ class AnsibleCloudStack: if self.case_sensitive_keys and key in self.case_sensitive_keys: if before_value != after_value: - self.result['diff']['before'][key] = before_value - self.result['diff']['after'][key] = after_value + if skip_diff_for_keys and key not in skip_diff_for_keys: + self.result['diff']['before'][key] = before_value + self.result['diff']['after'][key] = after_value result = True # Test for diff in case insensitive way elif before_value.lower() != after_value.lower(): - self.result['diff']['before'][key] = before_value - self.result['diff']['after'][key] = after_value + if skip_diff_for_keys and key not in skip_diff_for_keys: + self.result['diff']['before'][key] = before_value + self.result['diff']['after'][key] = after_value result = True else: - self.result['diff']['before'][key] = None - self.result['diff']['after'][key] = to_text(value) + if skip_diff_for_keys and key not in skip_diff_for_keys: + self.result['diff']['before'][key] = None + self.result['diff']['after'][key] = to_text(value) result = True return result @@ -212,7 +217,10 @@ class AnsibleCloudStack: self.fail_json(msg="Failed: '%s'" % res['errortext']) except CloudStackException as e: - self.fail_json(msg='CloudStackException: %s' % str(e)) + self.fail_json(msg='CloudStackException: %s' % to_native(e)) + + except Exception as e: + self.fail_json(msg=to_native(e)) return res diff --git a/lib/ansible/modules/cloud/cloudstack/cs_vpn_customer_gateway.py b/lib/ansible/modules/cloud/cloudstack/cs_vpn_customer_gateway.py new file mode 100644 index 00000000000..3c14eff6eb2 --- /dev/null +++ b/lib/ansible/modules/cloud/cloudstack/cs_vpn_customer_gateway.py @@ -0,0 +1,329 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# (c) 2017, René Moser +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = r''' +--- +module: cs_vpn_customer_gateway +short_description: Manages site-to-site VPN customer gateway configurations on Apache CloudStack based clouds. +description: + - Create, update and remove VPN customer gateways. +version_added: "2.5" +author: "René Moser (@resmo)" +options: + name: + description: + - Name of the gateway. + required: true + cidrs: + description: + - List of guest CIDRs behind the gateway. + - Required if C(state=present). + aliases: [ cidr ] + gateway: + description: + - Public IP address of the gateway. + - Required if C(state=present). + esp_policy: + description: + - ESP policy in the format e.g. C(aes256-sha1;modp1536). + - Required if C(state=present). + ike_policy: + description: + - IKE policy in the format e.g. C(aes256-sha1;modp1536). + - Required if C(state=present). + ipsec_psk: + description: + - IPsec Preshared-Key. + - Cannot contain newline or double quotes. + - Required if C(state=present). + ike_lifetime: + description: + - Lifetime in seconds of phase 1 VPN connection. + - Defaulted to 86400 by the API on creation if not set. + esp_lifetime: + description: + - Lifetime in seconds of phase 2 VPN connection. + - Defaulted to 3600 by the API on creation if not set. + dpd: + description: + - Enable Dead Peer Detection. + - Disabled per default by the API on creation if not set. + choices: [ yes, no ] + force_encap: + description: + - Force encapsulation for NAT traversal. + - Disabled per default by the API on creation if not set. + choices: [ yes, no ] + state: + description: + - State of the VPN customer gateway. + default: present + choices: [ present, absent ] + domain: + description: + - Domain the VPN customer gateway is related to. + account: + description: + - Account the VPN customer gateway is related to. + project: + description: + - Name of the project the VPN gateway is related to. + poll_async: + description: + - Poll async jobs until job has finished. + default: true +extends_documentation_fragment: cloudstack +''' + +EXAMPLES = r''' +- name: Create a vpn customer gateway + local_action: + module: cs_vpn_customer_gateway + name: my vpn customer gateway + cidrs: + - 192.168.123.0/24 + - 192.168.124.0/24 + esp_policy: aes256-sha1;modp1536 + gateway: 10.10.1.1 + ike_policy: aes256-sha1;modp1536 + ipsec_psk: "S3cr3Tk3Y" + +- name: Remove a vpn customer gateway + local_action: + module: cs_vpn_customer_gateway + name: my vpn customer gateway + state: absent +''' + +RETURN = r''' +--- +id: + description: UUID of the VPN customer gateway. + returned: success + type: string + sample: 04589590-ac63-4ffc-93f5-b698b8ac38b6 +gateway: + description: IP address of the VPN customer gateway. + returned: success + type: string + sample: 10.100.212.10 +domain: + description: Domain the VPN customer gateway is related to. + returned: success + type: string + sample: example domain +account: + description: Account the VPN customer gateway is related to. + returned: success + type: string + sample: example account +project: + description: Name of project the VPN customer gateway is related to. + returned: success + type: string + sample: Production +dpd: + description: Whether dead pear detection is enabled or not. + returned: success + type: bool + sample: true +esp_lifetime: + description: Lifetime in seconds of phase 2 VPN connection. + returned: success + type: int + sample: 86400 +esp_policy: + description: IKE policy of the VPN customer gateway. + returned: success + type: string + sample: aes256-sha1;modp1536 +force_encap: + description: Whether encapsulation for NAT traversal is enforced or not. + returned: success + type: bool + sample: true +ike_lifetime: + description: Lifetime in seconds of phase 1 VPN connection. + returned: success + type: int + sample: 86400 +ike_policy: + description: ESP policy of the VPN customer gateway. + returned: success + type: string + sample: aes256-sha1;modp1536 +name: + description: Name of this customer gateway. + returned: success + type: string + sample: my vpn customer gateway +''' + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.cloudstack import ( + AnsibleCloudStack, + cs_argument_spec, + cs_required_together +) + + +class AnsibleCloudStackVpnCustomerGateway(AnsibleCloudStack): + + def __init__(self, module): + super(AnsibleCloudStackVpnCustomerGateway, self).__init__(module) + self.returns = { + 'dpd': 'dpd', + 'esplifetime': 'esp_lifetime', + 'esppolicy': 'esp_policy', + 'gateway': 'gateway', + 'ikepolicy': 'ike_policy', + 'ikelifetime': 'ike_lifetime', + 'ipaddress': 'ip_address', + } + + def _common_args(self): + return { + 'name': self.module.params.get('name'), + 'account': self.get_account(key='name'), + 'domainid': self.get_domain(key='id'), + 'projectid': self.get_project(key='id'), + 'cidrlist': ','.join(self.module.params.get('cidrs')) if self.module.params.get('cidrs') is not None else None, + 'esppolicy': self.module.params.get('esp_policy'), + 'esplifetime': self.module.params.get('esp_lifetime'), + 'ikepolicy': self.module.params.get('ike_policy'), + 'ikelifetime': self.module.params.get('ike_lifetime'), + 'ipsecpsk': self.module.params.get('ipsec_psk'), + 'dpd': self.module.params.get('dpd'), + 'forceencap': self.module.params.get('force_encap'), + 'gateway': self.module.params.get('gateway'), + } + + def get_vpn_customer_gateway(self): + args = { + 'account': self.get_account(key='name'), + 'domainid': self.get_domain(key='id'), + 'projectid': self.get_project(key='id') + } + vpn_customer_gateway = self.module.params.get('name') + vpn_customer_gateways = self.query_api('listVpnCustomerGateways', **args) + if vpn_customer_gateways: + for vgw in vpn_customer_gateways['vpncustomergateway']: + if vpn_customer_gateway.lower() in [vgw['id'], vgw['name'].lower()]: + return vgw + + def present_vpn_customer_gateway(self): + vpn_customer_gateway = self.get_vpn_customer_gateway() + required_params = [ + 'cidrs', + 'esp_policy', + 'gateway', + 'ike_policy', + 'ipsec_psk', + ] + self.module.fail_on_missing_params(required_params=required_params) + + if not vpn_customer_gateway: + vpn_customer_gateway = self._create_vpn_customer_gateway(vpn_customer_gateway) + else: + vpn_customer_gateway = self._update_vpn_customer_gateway(vpn_customer_gateway) + + return vpn_customer_gateway + + def _create_vpn_customer_gateway(self, vpn_customer_gateway): + self.result['changed'] = True + args = self._common_args() + if not self.module.check_mode: + res = self.query_api('createVpnCustomerGateway', **args) + poll_async = self.module.params.get('poll_async') + if poll_async: + vpn_customer_gateway = self.poll_job(res, 'vpncustomergateway') + return vpn_customer_gateway + + def _update_vpn_customer_gateway(self, vpn_customer_gateway): + args = self._common_args() + args.update({'id': vpn_customer_gateway['id']}) + if self.has_changed(args, vpn_customer_gateway, skip_diff_for_keys=['ipsecpsk']): + self.result['changed'] = True + if not self.module.check_mode: + res = self.query_api('updateVpnCustomerGateway', **args) + poll_async = self.module.params.get('poll_async') + if poll_async: + vpn_customer_gateway = self.poll_job(res, 'vpncustomergateway') + return vpn_customer_gateway + + def absent_vpn_customer_gateway(self): + vpn_customer_gateway = self.get_vpn_customer_gateway() + if vpn_customer_gateway: + self.result['changed'] = True + args = { + 'id': vpn_customer_gateway['id'] + } + if not self.module.check_mode: + res = self.query_api('deleteVpnCustomerGateway', **args) + poll_async = self.module.params.get('poll_async') + if poll_async: + self.poll_job(res, 'vpncustomergateway') + + return vpn_customer_gateway + + def get_result(self, vpn_customer_gateway): + super(AnsibleCloudStackVpnCustomerGateway, self).get_result(vpn_customer_gateway) + if vpn_customer_gateway: + if 'cidrlist' in vpn_customer_gateway: + self.result['cidrs'] = vpn_customer_gateway['cidrlist'].split(',') or [vpn_customer_gateway['cidrlist']] + # Ensure we return a bool + self.result['force_encap'] = True if vpn_customer_gateway['forceencap'] else False + return self.result + + +def main(): + argument_spec = cs_argument_spec() + argument_spec.update(dict( + name=dict(required=True), + state=dict(choices=['present', 'absent'], default='present'), + domain=dict(), + account=dict(), + project=dict(), + cidrs=dict(type='list', aliases=['cidr']), + esp_policy=dict(), + esp_lifetime=dict(type='int'), + gateway=dict(), + ike_policy=dict(), + ike_lifetime=dict(type='int'), + ipsec_psk=dict(no_log=True), + dpd=dict(type='bool'), + force_encap=dict(type='bool'), + poll_async=dict(type='bool', default=True), + )) + + module = AnsibleModule( + argument_spec=argument_spec, + required_together=cs_required_together(), + supports_check_mode=True + ) + + acs_vpn_cgw = AnsibleCloudStackVpnCustomerGateway(module) + + state = module.params.get('state') + if state == "absent": + vpn_customer_gateway = acs_vpn_cgw.absent_vpn_customer_gateway() + else: + vpn_customer_gateway = acs_vpn_cgw.present_vpn_customer_gateway() + + result = acs_vpn_cgw.get_result(vpn_customer_gateway) + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/test/integration/targets/cs_vpn_customer_gateway/aliases b/test/integration/targets/cs_vpn_customer_gateway/aliases new file mode 100644 index 00000000000..ee8454c6d12 --- /dev/null +++ b/test/integration/targets/cs_vpn_customer_gateway/aliases @@ -0,0 +1,2 @@ +cloud/cs +posix/ci/cloud/group1/cs diff --git a/test/integration/targets/cs_vpn_customer_gateway/meta/main.yml b/test/integration/targets/cs_vpn_customer_gateway/meta/main.yml new file mode 100644 index 00000000000..e9a5b9eeaef --- /dev/null +++ b/test/integration/targets/cs_vpn_customer_gateway/meta/main.yml @@ -0,0 +1,3 @@ +--- +dependencies: + - cs_common diff --git a/test/integration/targets/cs_vpn_customer_gateway/tasks/main.yml b/test/integration/targets/cs_vpn_customer_gateway/tasks/main.yml new file mode 100644 index 00000000000..d18d4b6fe13 --- /dev/null +++ b/test/integration/targets/cs_vpn_customer_gateway/tasks/main.yml @@ -0,0 +1,208 @@ +--- +- name: setup vpn customer gateway absent + cs_vpn_customer_gateway: + name: ansible_vpn_customer_gw + state: absent + register: vcg +- name: verify setup vpn customer gateway absent + assert: + that: + - vcg is successful + +- name: test create vpn customer gateway in check mode + cs_vpn_customer_gateway: + name: ansible_vpn_customer_gw + cidr: 192.168.123.0/24 + esp_policy: aes256-sha1;modp1536 + gateway: 10.10.0.1 + ike_policy: aes256-sha1;modp1536 + ipsec_psk: ~S3¢r3Tk3Y¼ + esp_lifetime: 3600 + check_mode: true + register: vcg +- name: verify test create vpn customer gateway in check mode + assert: + that: + - vcg is changed + +- name: test create vpn customer gateway + cs_vpn_customer_gateway: + name: ansible_vpn_customer_gw + cidr: 192.168.123.0/24 + esp_policy: aes256-sha1;modp1536 + gateway: 10.10.0.1 + ike_policy: aes256-sha1;modp1536 + ipsec_psk: ~S3¢r3Tk3Y¼ + esp_lifetime: 3600 + register: vcg +- name: verify test create vpn customer gateway + assert: + that: + - vcg is changed + - "vcg.cidrs == ['192.168.123.0/24']" + - vcg.dpd == false + - vcg.esp_lifetime == 3600 + - vcg.esp_policy == 'aes256-sha1;modp1536' + - vcg.force_encap == false + - vcg.ike_policy == 'aes256-sha1;modp1536' + - vcg.gateway == '10.10.0.1' + - vcg.name == 'ansible_vpn_customer_gw' + - vcg.ike_lifetime == 86400 + +- name: test create vpn customer gateway idempotency + cs_vpn_customer_gateway: + name: ansible_vpn_customer_gw + cidr: 192.168.123.0/24 + esp_policy: aes256-sha1;modp1536 + gateway: 10.10.0.1 + ike_policy: aes256-sha1;modp1536 + ipsec_psk: ~S3¢r3Tk3Y¼ + esp_lifetime: 3600 + register: vcg +- name: verify test create vpn customer gateway idempotency + assert: + that: + - vcg is not changed + - "vcg.cidrs == ['192.168.123.0/24']" + - vcg.dpd == false + - vcg.esp_lifetime == 3600 + - vcg.esp_policy == 'aes256-sha1;modp1536' + - vcg.force_encap == false + - vcg.ike_policy == 'aes256-sha1;modp1536' + - vcg.gateway == '10.10.0.1' + - vcg.name == 'ansible_vpn_customer_gw' + - vcg.ike_lifetime == 86400 + +- name: test update vpn customer gateway in check mode + cs_vpn_customer_gateway: + name: ansible_vpn_customer_gw + cidrs: + - 192.168.123.0/24 + - 192.168.124.0/24 + esp_policy: aes256-sha1;modp1536 + gateway: 10.10.1.1 + ike_policy: aes256-sha1;modp1536 + ipsec_psk: ~S3¢r3Tk3Y@ + esp_lifetime: 1800 + ike_lifetime: 23200 + force_encap: true + check_mode: true + register: vcg +- name: verify test update vpn customer gateway in check mode + assert: + that: + - vcg is changed + - "vcg.cidrs == ['192.168.123.0/24']" + - vcg.dpd == false + - vcg.esp_lifetime == 3600 + - vcg.esp_policy == 'aes256-sha1;modp1536' + - vcg.force_encap == false + - vcg.ike_policy == 'aes256-sha1;modp1536' + - vcg.gateway == '10.10.0.1' + - vcg.name == 'ansible_vpn_customer_gw' + - vcg.ike_lifetime == 86400 + +- name: test update vpn customer gateway + cs_vpn_customer_gateway: + name: ansible_vpn_customer_gw + cidrs: + - 192.168.123.0/24 + - 192.168.124.0/24 + esp_policy: aes256-sha1;modp1536 + gateway: 10.10.1.1 + ike_policy: aes256-sha1;modp1536 + ipsec_psk: ~S3¢r3Tk3Y@ + esp_lifetime: 1800 + ike_lifetime: 23200 + force_encap: true + register: vcg +- name: verify test update vpn customer gateway + assert: + that: + - vcg is changed + - "vcg.cidrs == ['192.168.123.0/24', '192.168.124.0/24']" + - vcg.dpd == false + - vcg.esp_lifetime == 1800 + - vcg.esp_policy == 'aes256-sha1;modp1536' + - vcg.force_encap == true + - vcg.ike_policy == 'aes256-sha1;modp1536' + - vcg.gateway == '10.10.1.1' + - vcg.name == 'ansible_vpn_customer_gw' + - vcg.ike_lifetime == 23200 + +- name: test update vpn customer gateway idempotence + cs_vpn_customer_gateway: + name: ansible_vpn_customer_gw + cidrs: + - 192.168.123.0/24 + - 192.168.124.0/24 + esp_policy: aes256-sha1;modp1536 + gateway: 10.10.1.1 + ike_policy: aes256-sha1;modp1536 + ipsec_psk: ~S3¢r3Tk3Y@ + esp_lifetime: 1800 + ike_lifetime: 23200 + force_encap: true + register: vcg +- name: verify test update vpn customer gateway idempotence + assert: + that: + - vcg is not changed + - "vcg.cidrs == ['192.168.123.0/24', '192.168.124.0/24']" + - vcg.dpd == false + - vcg.esp_lifetime == 1800 + - vcg.esp_policy == 'aes256-sha1;modp1536' + - vcg.force_encap == true + - vcg.ike_policy == 'aes256-sha1;modp1536' + - vcg.gateway == '10.10.1.1' + - vcg.name == 'ansible_vpn_customer_gw' + - vcg.ike_lifetime == 23200 + +- name: test remove vpn customer gateway in check mode + cs_vpn_customer_gateway: + name: ansible_vpn_customer_gw + state: absent + check_mode: true + register: vcg +- name: verify test remove vpn customer gateway in check mode + assert: + that: + - vcg is changed + - "vcg.cidrs == ['192.168.123.0/24', '192.168.124.0/24']" + - vcg.dpd == false + - vcg.esp_lifetime == 1800 + - vcg.esp_policy == 'aes256-sha1;modp1536' + - vcg.force_encap == true + - vcg.ike_policy == 'aes256-sha1;modp1536' + - vcg.gateway == '10.10.1.1' + - vcg.name == 'ansible_vpn_customer_gw' + - vcg.ike_lifetime == 23200 + +- name: test remove vpn customer gateway + cs_vpn_customer_gateway: + name: ansible_vpn_customer_gw + state: absent + register: vcg +- name: verify test remove vpn customer gateway + assert: + that: + - vcg is changed + - "vcg.cidrs == ['192.168.123.0/24', '192.168.124.0/24']" + - vcg.dpd == false + - vcg.esp_lifetime == 1800 + - vcg.esp_policy == 'aes256-sha1;modp1536' + - vcg.force_encap == true + - vcg.ike_policy == 'aes256-sha1;modp1536' + - vcg.gateway == '10.10.1.1' + - vcg.name == 'ansible_vpn_customer_gw' + - vcg.ike_lifetime == 23200 + +- name: test remove vpn customer gateway idempotence + cs_vpn_customer_gateway: + name: ansible_vpn_customer_gw + state: absent + register: vcg +- name: verify test remove vpn customer gateway idempotence + assert: + that: + - vcg is not changed