From 43514e9d93455a94521dd0d1f5c29bf9e07ca5bf Mon Sep 17 00:00:00 2001 From: "Patryk D. Cichy" Date: Thu, 28 Mar 2019 21:53:32 +0100 Subject: [PATCH] Add a new CloudStack module - cs_traffic_type (#54451) * Add get_physical_network to AnsibleCloudStack * Add new module cs_traffic_type --- lib/ansible/module_utils/cloudstack.py | 18 + .../cloud/cloudstack/cs_traffic_type.py | 324 ++++++++++++++++++ .../targets/cs_traffic_type/aliases | 2 + .../targets/cs_traffic_type/meta/main.yml | 3 + .../targets/cs_traffic_type/tasks/main.yml | 173 ++++++++++ .../cloud/cloudstack/test_cs_traffic_type.py | 129 +++++++ 6 files changed, 649 insertions(+) create mode 100644 lib/ansible/modules/cloud/cloudstack/cs_traffic_type.py create mode 100644 test/integration/targets/cs_traffic_type/aliases create mode 100644 test/integration/targets/cs_traffic_type/meta/main.yml create mode 100644 test/integration/targets/cs_traffic_type/tasks/main.yml create mode 100644 test/units/modules/cloud/cloudstack/test_cs_traffic_type.py diff --git a/lib/ansible/module_utils/cloudstack.py b/lib/ansible/module_utils/cloudstack.py index eb1d866e08..61758bb937 100644 --- a/lib/ansible/module_utils/cloudstack.py +++ b/lib/ansible/module_utils/cloudstack.py @@ -302,6 +302,24 @@ class AnsibleCloudStack: self._vpc_networks_ids.append(n['id']) return network_id in self._vpc_networks_ids + def get_physical_network(self, key=None): + if self.physical_network: + return self._get_by_key(key, self.physical_network) + physical_network = self.module.params.get('physical_network') + args = { + 'zoneid': self.get_zone(key='id') + } + physical_networks = self.query_api('listPhysicalNetworks', **args) + if not physical_networks: + self.fail_json(msg="No physical networks available.") + + for net in physical_networks['physicalnetwork']: + if physical_network in [net['name'], net['id']]: + self.physical_network = net + self.result['physical_network'] = net['name'] + return self._get_by_key(key, self.physical_network) + self.fail_json(msg="Physical Network '%s' not found" % physical_network) + def get_network(self, key=None): """Return a network dictionary or the value of given key of.""" if self.network: diff --git a/lib/ansible/modules/cloud/cloudstack/cs_traffic_type.py b/lib/ansible/modules/cloud/cloudstack/cs_traffic_type.py new file mode 100644 index 0000000000..4443fb161a --- /dev/null +++ b/lib/ansible/modules/cloud/cloudstack/cs_traffic_type.py @@ -0,0 +1,324 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# (c) 2019, Patryk D. Cichy +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = ''' +--- +module: cs_traffic_type +short_description: Manages traffic types on CloudStack Physical Networks +description: + - Add, remove, update Traffic Types associated with CloudStack Physical Networks. +extends_documentation_fragment: cloudstack +version_added: "2.8" +author: + - Patryk Cichy (@PatTheSilent) +options: + physical_network: + description: + - the name of the Physical Network + required: true + type: str + zone: + description: + - Name of the zone with the physical network. + - Default zone will be used if this is empty. + type: str + traffic_type: + description: + - the trafficType to be added to the physical network. + required: true + choices: [Management, Guest, Public, Storage] + type: str + state: + description: + - State of the traffic type + choices: [present, absent] + default: present + type: str + hyperv_networklabel: + description: + - The network name label of the physical device dedicated to this traffic on a HyperV host. + type: str + isolation_method: + description: + - Use if the physical network has multiple isolation types and traffic type is public. + choices: [vlan, vxlan] + type: str + kvm_networklabel: + description: + - The network name label of the physical device dedicated to this traffic on a KVM host. + type: str + ovm3_networklabel: + description: + - The network name of the physical device dedicated to this traffic on an OVM3 host. + type: str + vlan: + description: + - The VLAN id to be used for Management traffic by VMware host. + type: str + vmware_networklabel: + description: + - The network name label of the physical device dedicated to this traffic on a VMware host. + type: str + xen_networklabel: + description: + - The network name label of the physical device dedicated to this traffic on a XenServer host. + type: str + poll_async: + description: + - Poll async jobs until job has finished. + default: yes + type: bool +''' + +EXAMPLES = ''' +- name: add a traffic type + cs_traffic_type: + physical_network: public-network + traffic_type: Guest + zone: test-zone + delegate_to: localhost + +- name: update traffic type + cs_traffic_type: + physical_network: public-network + traffic_type: Guest + kvm_networklabel: cloudbr0 + zone: test-zone + delegate_to: localhost + +- name: remove traffic type + cs_traffic_type: + physical_network: public-network + traffic_type: Public + state: absent + zone: test-zone + delegate_to: localhost +''' + +RETURN = ''' +--- +id: + description: ID of the network provider + returned: success + type: str + sample: 659c1840-9374-440d-a412-55ca360c9d3c +traffic_type: + description: the trafficType that was added to the physical network + returned: success + type: str + sample: Public +hyperv_networklabel: + description: The network name label of the physical device dedicated to this traffic on a HyperV host + returned: success + type: str + sample: HyperV Internal Switch +kvm_networklabel: + description: The network name label of the physical device dedicated to this traffic on a KVM host + returned: success + type: str + sample: cloudbr0 +ovm3_networklabel: + description: The network name of the physical device dedicated to this traffic on an OVM3 host + returned: success + type: str + sample: cloudbr0 +physical_network: + description: the physical network this belongs to + returned: success + type: str + sample: 28ed70b7-9a1f-41bf-94c3-53a9f22da8b6 +vmware_networklabel: + description: The network name label of the physical device dedicated to this traffic on a VMware host + returned: success + type: str + sample: Management Network +xen_networklabel: + description: The network name label of the physical device dedicated to this traffic on a XenServer host + returned: success + type: str + sample: xenbr0 +zone: + description: Name of zone the physical network is in. + returned: success + type: str + sample: ch-gva-2 +''' + +from ansible.module_utils.cloudstack import AnsibleCloudStack, cs_argument_spec, cs_required_together +from ansible.module_utils.basic import AnsibleModule + + +class AnsibleCloudStackTrafficType(AnsibleCloudStack): + + def __init__(self, module): + super(AnsibleCloudStackTrafficType, self).__init__(module) + self.returns = { + 'traffictype': 'traffic_type', + 'hypervnetworklabel': 'hyperv_networklabel', + 'kvmnetworklabel': 'kvm_networklabel', + 'ovm3networklabel': 'ovm3_networklabel', + 'physicalnetworkid': 'physical_network', + 'vmwarenetworklabel': 'vmware_networklabel', + 'xennetworklabel': 'xen_networklabel' + } + + self.traffic_type = None + + def _get_label_args(self): + label_args = dict() + if self.module.params.get('hyperv_networklabel'): + label_args.update(dict(hypervnetworklabel=self.module.params.get('hyperv_networklabel'))) + if self.module.params.get('kvm_networklabel'): + label_args.update(dict(kvmnetworklabel=self.module.params.get('kvm_networklabel'))) + if self.module.params.get('ovm3_networklabel'): + label_args.update(dict(ovm3networklabel=self.module.params.get('ovm3_networklabel'))) + if self.module.params.get('vmware_networklabel'): + label_args.update(dict(vmwarenetworklabel=self.module.params.get('vmware_networklabel'))) + return label_args + + def _get_additional_args(self): + additional_args = dict() + + if self.module.params.get('isolation_method'): + additional_args.update(dict(isolationmethod=self.module.params.get('isolation_method'))) + + if self.module.params.get('vlan'): + additional_args.update(dict(vlan=self.module.params.get('vlan'))) + + additional_args.update(self._get_label_args()) + + return additional_args + + def get_traffic_types(self): + args = { + 'physicalnetworkid': self.get_physical_network(key='id') + } + traffic_types = self.query_api('listTrafficTypes', **args) + return traffic_types + + def get_traffic_type(self): + if self.traffic_type: + return self.traffic_type + + traffic_type = self.module.params.get('traffic_type') + + traffic_types = self.get_traffic_types() + + if traffic_types: + for t_type in traffic_types['traffictype']: + if traffic_type.lower() in [t_type['traffictype'].lower(), t_type['id']]: + self.traffic_type = t_type + break + return self.traffic_type + + def present_traffic_type(self): + traffic_type = self.get_traffic_type() + if traffic_type: + self.traffic_type = self.update_traffic_type() + else: + self.result['changed'] = True + self.traffic_type = self.add_traffic_type() + + return self.traffic_type + + def add_traffic_type(self): + traffic_type = self.module.params.get('traffic_type') + args = { + 'physicalnetworkid': self.get_physical_network(key='id'), + 'traffictype': traffic_type + } + args.update(self._get_additional_args()) + if not self.module.check_mode: + resource = self.query_api('addTrafficType', **args) + poll_async = self.module.params.get('poll_async') + if poll_async: + self.traffic_type = self.poll_job(resource, 'traffictype') + return self.traffic_type + + def absent_traffic_type(self): + traffic_type = self.get_traffic_type() + if traffic_type: + + args = { + 'id': traffic_type['id'] + } + self.result['changed'] = True + if not self.module.check_mode: + resource = self.query_api('deleteTrafficType', **args) + poll_async = self.module.params.get('poll_async') + if poll_async: + self.poll_job(resource, 'traffictype') + + return traffic_type + + def update_traffic_type(self): + + traffic_type = self.get_traffic_type() + args = { + 'id': traffic_type['id'] + } + args.update(self._get_label_args()) + if self.has_changed(args, traffic_type): + self.result['changed'] = True + if not self.module.check_mode: + resource = self.query_api('updateTrafficType', **args) + poll_async = self.module.params.get('poll_async') + if poll_async: + self.traffic_type = self.poll_job(resource, 'traffictype') + + return self.traffic_type + + +def setup_module_object(): + argument_spec = cs_argument_spec() + argument_spec.update(dict( + physical_network=dict(required=True), + zone=dict(), + state=dict(choices=['present', 'absent'], default='present'), + traffic_type=dict(required=True, choices=['Management', 'Guest', 'Public', 'Storage']), + hyperv_networklabel=dict(), + isolation_method=dict(choices=['vlan', 'vxlan']), + kvm_networklabel=dict(), + ovm3_networklabel=dict(), + vlan=dict(), + vmware_networklabel=dict(), + xen_networklabel=dict(), + poll_async=dict(type='bool', default=True) + )) + + module = AnsibleModule( + argument_spec=argument_spec, + required_together=cs_required_together(), + supports_check_mode=True + ) + return module + + +def execute_module(module): + actt = AnsibleCloudStackTrafficType(module) + state = module.params.get('state') + + if state in ['present']: + result = actt.present_traffic_type() + else: + result = actt.absent_traffic_type() + + return actt.get_result(result) + + +def main(): + module = setup_module_object() + result = execute_module(module) + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/test/integration/targets/cs_traffic_type/aliases b/test/integration/targets/cs_traffic_type/aliases new file mode 100644 index 0000000000..c89c86d7d2 --- /dev/null +++ b/test/integration/targets/cs_traffic_type/aliases @@ -0,0 +1,2 @@ +cloud/cs +shippable/cs/group1 diff --git a/test/integration/targets/cs_traffic_type/meta/main.yml b/test/integration/targets/cs_traffic_type/meta/main.yml new file mode 100644 index 0000000000..e9a5b9eeae --- /dev/null +++ b/test/integration/targets/cs_traffic_type/meta/main.yml @@ -0,0 +1,3 @@ +--- +dependencies: + - cs_common diff --git a/test/integration/targets/cs_traffic_type/tasks/main.yml b/test/integration/targets/cs_traffic_type/tasks/main.yml new file mode 100644 index 0000000000..54e155de3a --- /dev/null +++ b/test/integration/targets/cs_traffic_type/tasks/main.yml @@ -0,0 +1,173 @@ +--- +# Create a new zone - the default one is enabled +- name: assure zone for tests + cs_zone: + name: cs-test-zone + state: present + dns1: 8.8.8.8 + network_type: advanced + register: cszone + +- name: ensure the zone is disabled + cs_zone: + name: "{{ cszone.name }}" + state: disabled + register: cszone + +- name: setup a network + cs_physical_network: + name: net01 + zone: "{{ cszone.name }}" + isolation_method: VLAN + broadcast_domain_range: ZONE + ignore_errors: true + register: pn + + +- name: fail on missing params + cs_traffic_type: + ignore_errors: true + register: tt +- name: validate fail on missing params + assert: + that: + - tt is failed + - 'tt.msg == "missing required arguments: physical_network, traffic_type"' + +- name: add a traffic type in check mode + cs_traffic_type: + physical_network: "{{ pn.name }}" + traffic_type: Guest + zone: "{{ pn.zone }}" + register: tt + check_mode: yes +- name: validate add a traffic type in check mode + assert: + that: + - tt is changed + - tt.zone == pn.zone + +- name: add a traffic type + cs_traffic_type: + physical_network: "{{ pn.name }}" + traffic_type: Guest + zone: "{{ pn.zone }}" + register: tt +- name: validate add a traffic type + assert: + that: + - tt is changed + - tt.physical_network == pn.id + - tt.traffic_type == 'Guest' + - tt.zone == pn.zone + +- name: add a traffic type idempotence + cs_traffic_type: + physical_network: "{{ pn.name }}" + traffic_type: Guest + zone: "{{ pn.zone }}" + register: tt +- name: validate add a traffic type idempotence + assert: + that: + - tt is not changed + - tt.physical_network == pn.id + - tt.traffic_type == 'Guest' + - tt.zone == pn.zone + +- name: update traffic type + cs_traffic_type: + physical_network: "{{ pn.name }}" + traffic_type: Guest + kvm_networklabel: cloudbr0 + zone: "{{ pn.zone }}" + register: tt +- name: validate update traffic type + assert: + that: + - tt is changed + - tt.physical_network == pn.id + - tt.traffic_type == 'Guest' + - tt.zone == pn.zone + - tt.kvm_networklabel == 'cloudbr0' + +- name: update traffic type idempotence + cs_traffic_type: + physical_network: "{{ pn.name }}" + traffic_type: Guest + kvm_networklabel: cloudbr0 + zone: "{{ pn.zone }}" + register: tt +- name: validate update traffic type idempotence + assert: + that: + - tt is not changed + - tt.physical_network == pn.id + - tt.traffic_type == 'Guest' + - tt.zone == pn.zone + - tt.kvm_networklabel == 'cloudbr0' + +- name: add a removable traffic type + cs_traffic_type: + physical_network: "{{ pn.name }}" + traffic_type: Public + kvm_networklabel: cloudbr1 + zone: "{{ pn.zone }}" + register: tt +- name: validate add a removable traffic type + assert: + that: + - tt is changed + - tt.physical_network == pn.id + - tt.traffic_type == 'Public' + - tt.zone == pn.zone + - tt.kvm_networklabel == 'cloudbr1' + +- name: remove traffic type in check mode + cs_traffic_type: + physical_network: "{{ pn.name }}" + traffic_type: Public + state: absent + zone: "{{ pn.zone }}" + check_mode: yes + register: tt +- name: validate remove traffic type in check mode + assert: + that: + - tt is changed + +- name: remove traffic type + cs_traffic_type: + physical_network: "{{ pn.name }}" + traffic_type: Public + state: absent + zone: "{{ pn.zone }}" + register: tt +- name: validate remove traffic type + assert: + that: + - tt is changed + - tt.zone == pn.zone + +- name: remove traffic type idempotence + cs_traffic_type: + physical_network: "{{ pn.name }}" + traffic_type: Public + state: absent + zone: "{{ pn.zone }}" + register: tt +- name: validate + assert: + that: + - tt is not changed + - tt.zone == pn.zone + +- name: cleanup + block: + - cs_physical_network: + name: "{{ pn.name }}" + zone: "{{ cszone.name }}" + state: absent + - cs_zone: + name: "{{ cszone.name }}" + state: absent \ No newline at end of file diff --git a/test/units/modules/cloud/cloudstack/test_cs_traffic_type.py b/test/units/modules/cloud/cloudstack/test_cs_traffic_type.py new file mode 100644 index 0000000000..477bc70472 --- /dev/null +++ b/test/units/modules/cloud/cloudstack/test_cs_traffic_type.py @@ -0,0 +1,129 @@ +import sys + +import units.compat.unittest as unittest +from units.compat.mock import MagicMock +from units.compat.unittest import TestCase +from units.modules.utils import set_module_args + +# Exoscale's cs doesn't support Python 2.6 +if sys.version_info[:2] != (2, 6): + from ansible.modules.cloud.cloudstack.cs_traffic_type import AnsibleCloudStackTrafficType, setup_module_object + + +EXISTING_TRAFFIC_TYPES_RESPONSE = { + "count": 3, + "traffictype": [ + { + "id": "9801cf73-5a73-4883-97e4-fa20c129226f", + "kvmnetworklabel": "cloudbr0", + "physicalnetworkid": "659c1840-9374-440d-a412-55ca360c9d3c", + "traffictype": "Management" + }, + { + "id": "28ed70b7-9a1f-41bf-94c3-53a9f22da8b6", + "kvmnetworklabel": "cloudbr0", + "physicalnetworkid": "659c1840-9374-440d-a412-55ca360c9d3c", + "traffictype": "Guest" + }, + { + "id": "9c05c802-84c0-4eda-8f0a-f681364ffb46", + "kvmnetworklabel": "cloudbr0", + "physicalnetworkid": "659c1840-9374-440d-a412-55ca360c9d3c", + "traffictype": "Storage" + } + ] +} + +VALID_LIST_NETWORKS_RESPONSE = { + "count": 1, + "physicalnetwork": [ + { + "broadcastdomainrange": "ZONE", + "id": "659c1840-9374-440d-a412-55ca360c9d3c", + "name": "eth1", + "state": "Enabled", + "vlan": "3900-4000", + "zoneid": "49acf813-a8dd-4da0-aa53-1d826d6003e7" + } + ] +} + +VALID_LIST_ZONES_RESPONSE = { + "count": 1, + "zone": [ + { + "allocationstate": "Enabled", + "dhcpprovider": "VirtualRouter", + "dns1": "8.8.8.8", + "dns2": "8.8.4.4", + "guestcidraddress": "10.10.0.0/16", + "id": "49acf813-a8dd-4da0-aa53-1d826d6003e7", + "internaldns1": "192.168.56.1", + "localstorageenabled": True, + "name": "DevCloud-01", + "networktype": "Advanced", + "securitygroupsenabled": False, + "tags": [], + "zonetoken": "df20d65a-c6c8-3880-9064-4f77de2291ef" + } + ] +} + + +base_module_args = { + "api_key": "api_key", + "api_secret": "very_secret_content", + "api_url": "http://localhost:8888/api/client", + "kvm_networklabel": "cloudbr0", + "physical_network": "eth1", + "poll_async": True, + "state": "present", + "traffic_type": "Guest", + "zone": "DevCloud-01" +} + + +class TestAnsibleCloudstackTraffiType(TestCase): + + @unittest.skipUnless(sys.version_info[:2] >= (2, 7), "Exoscale's cs doesn't support Python 2.6") + def test_module_is_created_sensibly(self): + set_module_args(base_module_args) + module = setup_module_object() + assert module.params['traffic_type'] == 'Guest' + + @unittest.skipUnless(sys.version_info[:2] >= (2, 7), "Exoscale's cs doesn't support Python 2.6") + def test_update_called_when_traffic_type_exists(self): + set_module_args(base_module_args) + module = setup_module_object() + actt = AnsibleCloudStackTrafficType(module) + actt.get_traffic_type = MagicMock(return_value=EXISTING_TRAFFIC_TYPES_RESPONSE['traffictype'][0]) + actt.update_traffic_type = MagicMock() + actt.present_traffic_type() + self.assertTrue(actt.update_traffic_type.called) + + @unittest.skipUnless(sys.version_info[:2] >= (2, 7), "Exoscale's cs doesn't support Python 2.6") + def test_update_not_called_when_traffic_type_doesnt_exist(self): + set_module_args(base_module_args) + module = setup_module_object() + actt = AnsibleCloudStackTrafficType(module) + actt.get_traffic_type = MagicMock(return_value=None) + actt.update_traffic_type = MagicMock() + actt.add_traffic_type = MagicMock() + actt.present_traffic_type() + self.assertFalse(actt.update_traffic_type.called) + self.assertTrue(actt.add_traffic_type.called) + + @unittest.skipUnless(sys.version_info[:2] >= (2, 7), "Exoscale's cs doesn't support Python 2.6") + def test_traffic_type_returned_if_exists(self): + set_module_args(base_module_args) + module = setup_module_object() + actt = AnsibleCloudStackTrafficType(module) + actt.get_physical_network = MagicMock(return_value=VALID_LIST_NETWORKS_RESPONSE['physicalnetwork'][0]) + actt.get_traffic_types = MagicMock(return_value=EXISTING_TRAFFIC_TYPES_RESPONSE) + tt = actt.present_traffic_type() + self.assertTrue(tt.get('kvmnetworklabel') == base_module_args['kvm_networklabel']) + self.assertTrue(tt.get('traffictype') == base_module_args['traffic_type']) + + +if __name__ == '__main__': + unittest.main()