From 01091cddf6d85ffb584de99ae2d59b693fabaf0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Moser?= Date: Wed, 10 Jan 2018 11:15:34 +0100 Subject: [PATCH] cloudstack: new module cs_vpn_connection (#34669) * cloudstack: new module cs_vpn_connection * cloudstack: add cs_vpn_connection tests * fix typos * remove local action in tests --- .../cloud/cloudstack/cs_vpn_connection.py | 340 ++++++++++++++++++ .../targets/cs_vpn_connection/aliases | 2 + .../targets/cs_vpn_connection/meta/main.yml | 3 + .../targets/cs_vpn_connection/tasks/main.yml | 200 +++++++++++ 4 files changed, 545 insertions(+) create mode 100644 lib/ansible/modules/cloud/cloudstack/cs_vpn_connection.py create mode 100644 test/integration/targets/cs_vpn_connection/aliases create mode 100644 test/integration/targets/cs_vpn_connection/meta/main.yml create mode 100644 test/integration/targets/cs_vpn_connection/tasks/main.yml diff --git a/lib/ansible/modules/cloud/cloudstack/cs_vpn_connection.py b/lib/ansible/modules/cloud/cloudstack/cs_vpn_connection.py new file mode 100644 index 00000000000..4569ce02ef7 --- /dev/null +++ b/lib/ansible/modules/cloud/cloudstack/cs_vpn_connection.py @@ -0,0 +1,340 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright (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_connection +short_description: Manages site-to-site VPN connections on Apache CloudStack based clouds. +description: + - Create and remove VPN connections. +version_added: "2.5" +author: "René Moser (@resmo)" +options: + vpc: + description: + - Name of the VPC the VPN connection is related to. + required: true + vpn_customer_gateway: + description: + - Name of the VPN connection. + - Required when C(state=present). + passive: + description: + - State of the VPN connection. + - Only considered when C(state=present). + default: no + choices: [ yes, no ] + force: + description: + - Activate the VPN gateway if not already activated on C(state=present). + - Also see M(cs_vpn_gateway). + default: no + choices: [ yes, no ] + state: + description: + - State of the VPN connection. + default: present + choices: [ present, absent ] + domain: + description: + - Domain the VPN connection is related to. + account: + description: + - Account the VPN connection is related to. + project: + description: + - Name of the project the VPN connection 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 connection with activated VPN gateway + local_action: + module: cs_vpn_connection + vpn_customer_gateway: my vpn connection + vpc: my vpc + +- name: Create a VPN connection and force VPN gateway activation + local_action: + module: cs_vpn_connection + vpn_customer_gateway: my vpn connection + vpc: my vpc + force: yes + +- name: Remove a vpn connection + local_action: + module: cs_vpn_connection + vpc: my vpc + state: absent +''' + +RETURN = r''' +--- +id: + description: UUID of the VPN connection. + returned: success + type: string + sample: 04589590-ac63-4ffc-93f5-b698b8ac38b6 +vpn_gateway_id: + description: UUID of the VPN gateway. + returned: success + type: string + sample: 04589590-ac63-93f5-4ffc-b698b8ac38b6 +domain: + description: Domain the VPN connection is related to. + returned: success + type: string + sample: example domain +account: + description: Account the VPN connection is related to. + returned: success + type: string + sample: example account +project: + description: Name of project the VPN connection is related to. + returned: success + type: string + sample: Production +created: + description: Date the connection was created. + returned: success + type: string + sample: 2014-12-01T14:57:57+0100 +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 connection. + 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 connection. + returned: success + type: string + sample: aes256-sha1;modp1536 +cidrs: + description: List of CIDRs of the customer gateway. + returned: success + type: list + sample: [ 10.10.10.0/24 ] +passive: + description: Whether the connection is passive or not. + returned: success + type: bool + sample: false +public_ip: + description: IP address of the VPN gateway. + returned: success + type: string + sample: 10.100.212.10 +gateway: + description: IP address of the VPN customer gateway. + returned: success + type: string + sample: 10.101.214.10 +state: + description: State of the VPN connection. + returned: success + type: string + sample: Connected +''' + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.cloudstack import ( + AnsibleCloudStack, + cs_argument_spec, + cs_required_together +) + + +class AnsibleCloudStackVpnConnection(AnsibleCloudStack): + + def __init__(self, module): + super(AnsibleCloudStackVpnConnection, self).__init__(module) + self.returns = { + 'dpd': 'dpd', + 'esplifetime': 'esp_lifetime', + 'esppolicy': 'esp_policy', + 'gateway': 'gateway', + 'ikepolicy': 'ike_policy', + 'ikelifetime': 'ike_lifetime', + 'publicip': 'public_ip', + 'passive': 'passive', + 's2svpngatewayid': 'vpn_gateway_id', + } + self.vpn_customer_gateway = None + + def get_vpn_customer_gateway(self, key=None): + vpn_customer_gateway = self.module.params.get('vpn_customer_gateway') + + if self.vpn_customer_gateway: + return self._get_by_key(key, self.vpn_customer_gateway) + + args = { + 'account': self.get_account(key='name'), + 'domainid': self.get_domain(key='id'), + 'projectid': self.get_project(key='id') + } + + vcgws = self.query_api('listVpnCustomerGateways', **args) + if vcgws: + for vcgw in vcgws['vpncustomergateway']: + if vpn_customer_gateway.lower() in [vcgw['id'], vcgw['name'].lower()]: + self.vpn_customer_gateway = vcgw + return self._get_by_key(key, self.vpn_customer_gateway) + self.fail_json(msg="VPN customer gateway not found: %s" % vpn_customer_gateway) + + def get_vpn_gateway(self, key=None): + args = { + 'vpcid': self.get_vpc(key='id'), + 'account': self.get_account(key='name'), + 'domainid': self.get_domain(key='id'), + 'projectid': self.get_project(key='id'), + } + vpn_gateways = self.query_api('listVpnGateways', **args) + if vpn_gateways: + return self._get_by_key(key, vpn_gateways['vpngateway'][0]) + + elif self.module.params.get('force'): + if self.module.check_mode: + return {} + res = self.query_api('createVpnGateway', **args) + vpn_gateway = self.poll_job(res, 'vpngateway') + return self._get_by_key(key, vpn_gateway) + + self.fail_json(msg="VPN gateway not found and not forced to create one") + + def get_vpn_connection(self): + args = { + 'vpcid': self.get_vpc(key='id'), + 'account': self.get_account(key='name'), + 'domainid': self.get_domain(key='id'), + 'projectid': self.get_project(key='id'), + } + + vpn_conns = self.query_api('listVpnConnections', **args) + if vpn_conns: + return vpn_conns['vpnconnection'][0] + + def present_vpn_connection(self): + vpn_conn = self.get_vpn_connection() + + args = { + 's2scustomergatewayid': self.get_vpn_customer_gateway(key='id'), + 's2svpngatewayid': self.get_vpn_gateway(key='id'), + 'passive': self.module.params.get('passive'), + } + + if not vpn_conn: + self.result['changed'] = True + + if not self.module.check_mode: + res = self.query_api('createVpnConnection', **args) + poll_async = self.module.params.get('poll_async') + if poll_async: + vpn_conn = self.poll_job(res, 'vpnconnection') + + return vpn_conn + + def absent_vpn_connection(self): + vpn_conn = self.get_vpn_connection() + + if vpn_conn: + self.result['changed'] = True + + args = { + 'id': vpn_conn['id'] + } + + if not self.module.check_mode: + res = self.query_api('deleteVpnConnection', **args) + poll_async = self.module.params.get('poll_async') + if poll_async: + self.poll_job(res, 'vpnconnection') + + return vpn_conn + + def get_result(self, vpn_conn): + super(AnsibleCloudStackVpnConnection, self).get_result(vpn_conn) + if vpn_conn: + if 'cidrlist' in vpn_conn: + self.result['cidrs'] = vpn_conn['cidrlist'].split(',') or [vpn_conn['cidrlist']] + # Ensure we return a bool + self.result['force_encap'] = True if vpn_conn['forceencap'] else False + + self.module.params['vpn_customer_gateway'] = vpn_conn['s2scustomergatewayid'] + self.result['vpn_customer_gateway'] = self.get_vpn_customer_gateway(key='name') + return self.result + + +def main(): + argument_spec = cs_argument_spec() + argument_spec.update(dict( + vpn_customer_gateway=dict(), + vpc=dict(required=True), + domain=dict(), + account=dict(), + project=dict(), + zone=dict(), + passive=dict(type='bool', default=False), + force=dict(type='bool', default=False), + state=dict(choices=['present', 'absent'], default='present'), + poll_async=dict(type='bool', default=True), + )) + + module = AnsibleModule( + argument_spec=argument_spec, + required_together=cs_required_together(), + required_if=[ + ('state', 'present', ['vpn_customer_gateway']), + ], + supports_check_mode=True + ) + + acs_vpn_conn = AnsibleCloudStackVpnConnection(module) + + state = module.params.get('state') + if state == "absent": + vpn_conn = acs_vpn_conn.absent_vpn_connection() + else: + vpn_conn = acs_vpn_conn.present_vpn_connection() + + result = acs_vpn_conn.get_result(vpn_conn) + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/test/integration/targets/cs_vpn_connection/aliases b/test/integration/targets/cs_vpn_connection/aliases new file mode 100644 index 00000000000..ee8454c6d12 --- /dev/null +++ b/test/integration/targets/cs_vpn_connection/aliases @@ -0,0 +1,2 @@ +cloud/cs +posix/ci/cloud/group1/cs diff --git a/test/integration/targets/cs_vpn_connection/meta/main.yml b/test/integration/targets/cs_vpn_connection/meta/main.yml new file mode 100644 index 00000000000..e9a5b9eeaef --- /dev/null +++ b/test/integration/targets/cs_vpn_connection/meta/main.yml @@ -0,0 +1,3 @@ +--- +dependencies: + - cs_common diff --git a/test/integration/targets/cs_vpn_connection/tasks/main.yml b/test/integration/targets/cs_vpn_connection/tasks/main.yml new file mode 100644 index 00000000000..c7bc060cc9a --- /dev/null +++ b/test/integration/targets/cs_vpn_connection/tasks/main.yml @@ -0,0 +1,200 @@ +--- +- name: setup vpc + cs_vpc: + name: my_vpc + display_text: my_vpc + cidr: 10.10.1.0/16 + zone: "{{ cs_common_zone_adv }}" + register: vpc +- name: verify setup vpc + assert: + that: + - vpc is successful + +- name: setup customer gateway + cs_vpn_customer_gateway: + name: my_vpn_customer_gateway + cidr: 192.168.123.0/24 + esp_policy: aes256-sha1;modp1536 + gateway: 10.11.1.1 + ike_policy: aes256-sha1;modp1536 + ipsec_psk: ~S3¢r3Tk3Y¼ + esp_lifetime: 3600 + register: vcg +- name: setup customer gateway + assert: + that: + - vcg is successful + +- name: setup remove vpn connection + cs_vpn_connection: + vpc: my_vpc + zone: "{{ cs_common_zone_adv }}" + state: absent + register: vpn_conn +- name: verify setup remove vpn connection + assert: + that: + - vpn_conn is successful + +- name: setup vpn gateway absent + cs_vpn_gateway: + vpc: my_vpc + zone: "{{ cs_common_zone_adv }}" + state: absent + register: vpn_gateway +- name: verify setup vpn gateway absent + assert: + that: + - vpn_gateway is successful + +- name: test fail create vpn connection without gateway and force + cs_vpn_connection: + vpn_customer_gateway: my_vpn_customer_gateway + vpc: my_vpc + zone: "{{ cs_common_zone_adv }}" + ignore_errors: yes + register: vpn_conn +- name: verify test fail create vpn connection without gateway and force + assert: + that: + - vpn_conn is failed + - vpn_conn.msg == "VPN gateway not found and not forced to create one" + +- name: test create vpn connection with force in check mode + cs_vpn_connection: + vpn_customer_gateway: my_vpn_customer_gateway + vpc: my_vpc + force: yes + zone: "{{ cs_common_zone_adv }}" + check_mode: yes + register: vpn_conn +- name: verify test create vpn connection with force in check mode + assert: + that: + - vpn_conn is changed + +- name: test create vpn connection with force + cs_vpn_connection: + vpn_customer_gateway: my_vpn_customer_gateway + vpc: my_vpc + force: yes + zone: "{{ cs_common_zone_adv }}" + register: vpn_conn +- name: verify test create vpn connection with force + assert: + that: + - vpn_conn is changed + - vpn_conn.vpn_customer_gateway == "my_vpn_customer_gateway" + - vpn_conn.vpc == "my_vpc" + +- name: test create vpn connection with force idempotence + cs_vpn_connection: + vpn_customer_gateway: my_vpn_customer_gateway + vpc: my_vpc + force: yes + zone: "{{ cs_common_zone_adv }}" + register: vpn_conn +- name: verify test create vpn connection with force idempotence + assert: + that: + - vpn_conn is not changed + - vpn_conn.vpn_customer_gateway == "my_vpn_customer_gateway" + - vpn_conn.vpc == "my_vpc" + +- name: test remove vpn connection in check mode + cs_vpn_connection: + vpc: my_vpc + zone: "{{ cs_common_zone_adv }}" + state: absent + check_mode: yes + register: vpn_conn +- name: verify test remove vpn connection in check mode + assert: + that: + - vpn_conn is changed + - vpn_conn.vpn_customer_gateway == "my_vpn_customer_gateway" + - vpn_conn.vpc == "my_vpc" + +- name: test remove vpn connection + cs_vpn_connection: + vpc: my_vpc + zone: "{{ cs_common_zone_adv }}" + state: absent + register: vpn_conn +- name: verify test remove vpn connection + assert: + that: + - vpn_conn is changed + - vpn_conn.vpn_customer_gateway == "my_vpn_customer_gateway" + - vpn_conn.vpc == "my_vpc" + +- name: test remove vpn connection idempotence + cs_vpn_connection: + vpc: my_vpc + zone: "{{ cs_common_zone_adv }}" + state: absent + register: vpn_conn +- name: verify test remove vpn connection idempotence + assert: + that: + - vpn_conn is not changed + +- name: setup create vpn gateway + cs_vpn_gateway: + vpc: my_vpc + zone: "{{ cs_common_zone_adv }}" + register: vpn_gateway +- name: verify setup create vpn gateway + assert: + that: + - vpn_gateway is success + +- name: test create vpn connection without force in check mode + cs_vpn_connection: + vpn_customer_gateway: my_vpn_customer_gateway + vpc: my_vpc + zone: "{{ cs_common_zone_adv }}" + check_mode: yes + register: vpn_conn +- name: verify test create vpn connection without force in check mode + assert: + that: + - vpn_conn is changed + +- name: test create vpn connection without force + cs_vpn_connection: + vpn_customer_gateway: my_vpn_customer_gateway + vpc: my_vpc + zone: "{{ cs_common_zone_adv }}" + register: vpn_conn +- name: verify test create vpn connection without force + assert: + that: + - vpn_conn is changed + - vpn_conn.vpn_customer_gateway == "my_vpn_customer_gateway" + - vpn_conn.vpc == "my_vpc" + +- name: test create vpn connection without force + cs_vpn_connection: + vpn_customer_gateway: my_vpn_customer_gateway + vpc: my_vpc + zone: "{{ cs_common_zone_adv }}" + register: vpn_conn +- name: verify test create vpn connection without force + assert: + that: + - vpn_conn is not changed + - vpn_conn.vpn_customer_gateway == "my_vpn_customer_gateway" + - vpn_conn.vpc == "my_vpc" + +- name: cleanup remove vpn connection + cs_vpn_connection: + vpc: my_vpc + zone: "{{ cs_common_zone_adv }}" + state: absent + register: vpn_conn +- name: verify cleanup remove vpn connection idempotence + assert: + that: + - vpn_conn is successful