From b2338f26ace253618863415dd4d31bcb9aa9c2ca Mon Sep 17 00:00:00 2001 From: techcon65 <82305863+techcon65@users.noreply.github.com> Date: Fri, 28 May 2021 10:48:51 +0530 Subject: [PATCH] New module: azure_rm_ipgroup (#528) * fixing update account_enabled bug in azure_rm_aduser.py (#524) * fixing ad related auth issue when using service pricinpal. (#525) * change class name of azure_rm_aduser (#526) * class are worngly named. fixed. * fixing sanity errors. * Add azure_rm_ipgroup module * Upgraded version_added * Removed log_mode and log_path * Addressed review comments Co-authored-by: haiyuan_zhang Co-authored-by: Fred-sun <37327967+Fred-sun@users.noreply.github.com> --- plugins/modules/azure_rm_ipgroup.py | 313 ++++++++++++++++++ plugins/modules/azure_rm_ipgroup_info.py | 201 +++++++++++ pr-pipelines.yml | 1 + .../targets/azure_rm_ipgroup/aliases | 3 + .../targets/azure_rm_ipgroup/meta/main.yml | 2 + .../targets/azure_rm_ipgroup/tasks/main.yml | 106 ++++++ 6 files changed, 626 insertions(+) create mode 100644 plugins/modules/azure_rm_ipgroup.py create mode 100644 plugins/modules/azure_rm_ipgroup_info.py create mode 100644 tests/integration/targets/azure_rm_ipgroup/aliases create mode 100644 tests/integration/targets/azure_rm_ipgroup/meta/main.yml create mode 100644 tests/integration/targets/azure_rm_ipgroup/tasks/main.yml diff --git a/plugins/modules/azure_rm_ipgroup.py b/plugins/modules/azure_rm_ipgroup.py new file mode 100644 index 000000000..3ab20f708 --- /dev/null +++ b/plugins/modules/azure_rm_ipgroup.py @@ -0,0 +1,313 @@ +#!/usr/bin/python +# +# Copyright (c) 2021 Aparna Patil(@techcon65) +# +# 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 + +DOCUMENTATION = ''' +--- +module: azure_rm_ipgroup + +version_added: "1.6.0" + +short_description: Create, delete and update IP group + +description: + - Creates, deletes, and updates IP group in specified resource group. + +options: + resource_group: + description: + - Name of the resource group. + required: true + type: str + name: + description: + - The name of the IP group. + required: true + type: str + location: + description: + - Location for IP group. Defaults to location of resource group if not specified. + type: str + ip_addresses: + description: + - The List of IP addresses in IP group. + type: list + elements: str + state: + description: + - Assert the state of the IP group. Use C(present) to create or update and C(absent) to delete. + default: present + type: str + choices: + - absent + - present + +extends_documentation_fragment: + - azure.azcollection.azure + - azure.azcollection.azure_tags + +author: + - Aparna Patil (@techcon65) +''' + +EXAMPLES = ''' +- name: Create IP Group + azure_rm_ipgroup: + resource_group: MyAzureResourceGroup + name: myipgroup + location: eastus + ip_addresses: + - 13.64.39.16/32 + - 40.74.146.80/31 + - 40.74.147.32/28 + tags: + key1: "value1" + state: present + +- name: Update IP Group + azure_rm_ipgroup: + resource_group: MyAzureResourceGroup + name: myipgroup + location: eastus + ip_addresses: + - 10.0.0.0/24 + tags: + key2: "value2" + +- name: Delete IP Group + azure_rm_ipgroup: + resource_group: MyAzureResourceGroup + name: myipgroup + state: absent +''' + +RETURN = ''' +state: + description: + - Current state of the IP group. + returned: always + type: complex + contains: + id: + description: + - The IP group ID. + returned: always + type: str + sample: "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/MyAzureResourceGroup/providers/ + Microsoft.Network/ipGroups/myipgroup" + name: + description: + - The IP group name. + returned: always + type: str + sample: 'myipgroup' + location: + description: + - The Azure Region where the resource lives. + returned: always + type: str + sample: eastus + ip_addresses: + description: + - The list of IP addresses in IP group. + returned: always + type: list + elements: str + sample: [ + "13.64.39.16/32", + "40.74.146.80/31", + "40.74.147.32/28" + ] + provisioning_state: + description: + - The provisioning state of the resource. + returned: always + type: str + sample: Succeeded + firewalls: + description: + - List of references to Firewall resources that this IpGroups is associated with. + returned: always + type: list + elements: dict + sample: [ + { + "id": "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/myAzureResourceGroup/providers/ + Microsoft.Network/azureFirewalls/azurefirewall" + } + ] + tags: + description: + - Resource tags. + returned: always + type: list + sample: [{"key1": "value1"}] + type: + description: + - The type of resource. + returned: always + type: str + sample: Microsoft.Network/IpGroups + etag: + description: + - The etag of the IP group. + returned: always + type: str + sample: c67388ea-6dab-481b-9387-bd441c0d32f8 +''' + +from ansible.module_utils.basic import _load_params +from ansible_collections.azure.azcollection.plugins.module_utils.azure_rm_common import AzureRMModuleBase, HAS_AZURE, \ + format_resource_id, normalize_location_name + +try: + from msrestazure.azure_exceptions import CloudError + from msrest.polling import LROPoller +except ImportError: + # This is handled in azure_rm_common + pass + + +class AzureRMIPGroup(AzureRMModuleBase): + + def __init__(self): + + _load_params() + # define user inputs from playbook + self.module_arg_spec = dict( + resource_group=dict(type='str', required=True), + name=dict(type='str', required=True), + location=dict(type='str'), + ip_addresses=dict(type='list', elements='str'), + state=dict(choices=['present', 'absent'], default='present', type='str') + ) + + self.results = dict( + changed=False, + state=dict() + ) + + self.resource_group = None + self.name = None + self.state = None + self.location = None + self.ip_addresses = None + self.tags = None + + super(AzureRMIPGroup, self).__init__(self.module_arg_spec, + supports_check_mode=True) + + def exec_module(self, **kwargs): + for key in list(self.module_arg_spec.keys()) + ['tags']: + setattr(self, key, kwargs[key]) + + changed = False + results = dict() + ip_group_old = None + ip_group_new = None + + # retrieve resource group to make sure it exists + resource_group = self.get_resource_group(self.resource_group) + if not self.location: + # Set default location + self.location = resource_group.location + + self.location = normalize_location_name(self.location) + + try: + self.log('Fetching IP group {0}'.format(self.name)) + ip_group_old = self.network_client.ip_groups.get(self.resource_group, self.name) + # serialize object into a dictionary + results = self.ipgroup_to_dict(ip_group_old) + if self.state == 'present': + changed = False + update_tags, results['tags'] = self.update_tags(results['tags']) + if update_tags: + changed = True + self.tags = results['tags'] + update_ip_address = self.ip_addresses_changed(self.ip_addresses, results['ip_addresses']) + if update_ip_address: + changed = True + results['ip_addresses'] = self.ip_addresses + elif self.state == 'absent': + changed = True + + except self.network_models.ErrorException: + if self.state == 'present': + changed = True + else: + changed = False + + self.results['changed'] = changed + self.results['state'] = results + + if self.check_mode: + return self.results + + if changed: + if self.state == 'present': + # create or update ip group + ip_group_new = \ + self.network_models.IpGroup(location=self.location, + ip_addresses=self.ip_addresses) + if self.tags: + ip_group_new.tags = self.tags + self.results['state'] = self.create_or_update_ipgroup(ip_group_new) + + elif self.state == 'absent': + # delete ip group + self.delete_ipgroup() + self.results['state'] = 'Deleted' + + return self.results + + def create_or_update_ipgroup(self, ip_group): + try: + # create ip group + response = self.network_client.ip_groups.create_or_update(resource_group_name=self.resource_group, + ip_groups_name=self.name, + parameters=ip_group) + if isinstance(response, LROPoller): + response = self.get_poller_result(response) + except Exception as exc: + self.fail("Error creating or updating IP group {0} - {1}".format(self.name, str(exc))) + return self.ipgroup_to_dict(response) + + def delete_ipgroup(self): + try: + # delete ip group + response = self.network_client.ip_groups.delete(resource_group_name=self.resource_group, + ip_groups_name=self.name) + if isinstance(response, LROPoller): + response = self.get_poller_result(response) + except Exception as exc: + self.fail("Error deleting IP group {0} - {1}".format(self.name, str(exc))) + return response + + def ip_addresses_changed(self, input_records, ip_group_records): + # comparing IP addresses list + + input_set = set(input_records) + ip_group_set = set(ip_group_records) + + changed = input_set != ip_group_set + + return changed + + def ipgroup_to_dict(self, ipgroup): + result = ipgroup.as_dict() + result['tags'] = ipgroup.tags + return result + + +def main(): + AzureRMIPGroup() + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/azure_rm_ipgroup_info.py b/plugins/modules/azure_rm_ipgroup_info.py new file mode 100644 index 000000000..5b0d10b9c --- /dev/null +++ b/plugins/modules/azure_rm_ipgroup_info.py @@ -0,0 +1,201 @@ +#!/usr/bin/python +# +# Copyright (c) 2021 Aparna Patil(@techcon65) +# +# 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 + +DOCUMENTATION = ''' +--- +module: azure_rm_ipgroup_info + +version_added: "1.6.0" + +short_description: Get IP group facts + +description: + - Get facts for specified IP group or all IP groups in a given resource group. + +options: + resource_group: + description: + - Name of the resource group. + type: str + name: + description: + - Name of the IP group. + type: str + tags: + description: + - Limit the results by providing resource tags. + type: dict + +extends_documentation_fragment: + - azure.azcollection.azure + - azure.azcollection.azure_tags + +author: + - Aparna Patil (@techcon65) + +''' + +EXAMPLES = ''' +- name: Get facts for one IP group + azure_rm_ipgroup_info: + resource_group: myAzureResourceGroup + name: myipgroup + +- name: Get facts for all IP groups in resource group + azure_rm_ipgroup_info: + resource_group: myAzureResourceGroup +''' + +RETURN = ''' +ipgroups: + description: + - Gets a list of IP groups. + returned: always + type: list + elements: dict + sample: [ + { + "etag": "c67388ea-6dab-481b-9387-bd441c0d32f8", + "firewalls": [], + "id": "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/MyAzureResourceGroup/providers/ + Microsoft.Network/ipGroups/myipgroup", + "ip_addresses": [ + "13.64.39.16/32", + "40.74.146.80/31", + "40.74.147.32/28" + ], + "location": "eastus", + "name": "myipgroup", + "provisioning_state": "Succeeded", + "tags": { + "key1": "value1" + } + } + ] +''' + +from ansible_collections.azure.azcollection.plugins.module_utils.azure_rm_common import AzureRMModuleBase + +try: + from msrestazure.azure_exceptions import CloudError + from azure.common import AzureMissingResourceHttpError, AzureHttpError +except Exception: + # This is handled in azure_rm_common + pass + +AZURE_OBJECT_CLASS = 'IpGroup' + + +class AzureRMIPGroupInfo(AzureRMModuleBase): + + def __init__(self): + + # define user inputs into argument + self.module_arg_spec = dict( + name=dict(type='str'), + resource_group=dict(type='str'), + tags=dict(type='dict') + ) + + # store the results of the module operation + self.results = dict( + changed=False + ) + + self.name = None + self.resource_group = None + self.tags = None + + super(AzureRMIPGroupInfo, self).__init__(self.module_arg_spec, supports_tags=True) + + def exec_module(self, **kwargs): + + for key in self.module_arg_spec: + setattr(self, key, kwargs[key]) + + results = [] + # list the conditions and results to return based on user input + if self.name is not None: + # if there is IP group name provided, return facts about that specific IP group + results = self.get_item() + elif self.resource_group: + # all the IP groups listed in specific resource group + results = self.list_resource_group() + else: + # all the IP groups in a subscription + results = self.list_items() + + self.results['ipgroups'] = self.curated_items(results) + + return self.results + + def get_item(self): + self.log('Get properties for {0}'.format(self.name)) + item = None + results = [] + # get specific IP group + try: + item = self.network_client.ip_groups.get(self.resource_group, self.name) + except self.network_models.ErrorException: + pass + + # serialize result + if item and self.has_tags(item.tags, self.tags): + results = [item] + return results + + def list_resource_group(self): + self.log('List all IP groups for resource group - {0}'.format(self.resource_group)) + try: + response = self.network_client.ip_groups.list_by_resource_group(self.resource_group) + except AzureHttpError as exc: + self.fail("Failed to list for resource group {0} - {1}".format(self.resource_group, str(exc))) + + results = [] + for item in response: + if self.has_tags(item.tags, self.tags): + results.append(item) + return results + + def list_items(self): + self.log('List all IP groups for a subscription ') + try: + response = self.network_client.ip_groups.list() + except AzureHttpError as exc: + self.fail("Failed to list all items - {0}".format(str(exc))) + + results = [] + for item in response: + if self.has_tags(item.tags, self.tags): + results.append(item) + return results + + def curated_items(self, raws): + return [self.ipgroup_to_dict(item) for item in raws] if raws else [] + + def ipgroup_to_dict(self, ipgroup): + result = dict( + id=ipgroup.id, + name=ipgroup.name, + location=ipgroup.location, + tags=ipgroup.tags, + ip_addresses=ipgroup.ip_addresses, + provisioning_state=ipgroup.provisioning_state, + firewalls=[dict(id=x.id) for x in ipgroup.firewalls], + etag=ipgroup.etag + ) + return result + + +def main(): + AzureRMIPGroupInfo() + + +if __name__ == '__main__': + main() diff --git a/pr-pipelines.yml b/pr-pipelines.yml index 18fa72f20..f5fe04c77 100644 --- a/pr-pipelines.yml +++ b/pr-pipelines.yml @@ -51,6 +51,7 @@ parameters: - "azure_rm_hdinsightcluster" - "azure_rm_image" - "azure_rm_iothub" + - "azure_rm_ipgroup" - "azure_rm_keyvault" - "azure_rm_keyvaultkey" - "azure_rm_keyvaultsecret" diff --git a/tests/integration/targets/azure_rm_ipgroup/aliases b/tests/integration/targets/azure_rm_ipgroup/aliases new file mode 100644 index 000000000..5d29c6c4d --- /dev/null +++ b/tests/integration/targets/azure_rm_ipgroup/aliases @@ -0,0 +1,3 @@ +cloud/azure +shippable/azure/group10 +destructive diff --git a/tests/integration/targets/azure_rm_ipgroup/meta/main.yml b/tests/integration/targets/azure_rm_ipgroup/meta/main.yml new file mode 100644 index 000000000..95e1952f9 --- /dev/null +++ b/tests/integration/targets/azure_rm_ipgroup/meta/main.yml @@ -0,0 +1,2 @@ +dependencies: + - setup_azure diff --git a/tests/integration/targets/azure_rm_ipgroup/tasks/main.yml b/tests/integration/targets/azure_rm_ipgroup/tasks/main.yml new file mode 100644 index 000000000..8656c7110 --- /dev/null +++ b/tests/integration/targets/azure_rm_ipgroup/tasks/main.yml @@ -0,0 +1,106 @@ +- name: Create IP group name + set_fact: + group_name: "ipgroup{{ resource_group | hash('md5') | truncate(22, True, '') }}" + +- name: Create IP group (check mode) + azure_rm_ipgroup: + resource_group: "{{ resource_group }}" + name: "{{ group_name }}" + location: eastus + ip_addresses: + - 13.64.39.16/32 + - 40.74.146.80/31 + - 40.74.147.32/28 + tags: + key1: "value1" + state: present + check_mode: yes + +- name: Create IP group + azure_rm_ipgroup: + resource_group: "{{ resource_group }}" + name: "{{ group_name }}" + location: eastus + ip_addresses: + - 13.64.39.16/32 + - 40.74.146.80/31 + - 40.74.147.32/28 + tags: + key1: "value1" + state: present + register: results + +- name: Assert that IP group is created + assert: + that: results.changed + +- name: Create same IP group again (Idempotent test) + azure_rm_ipgroup: + resource_group: "{{ resource_group }}" + name: "{{ group_name }}" + location: eastus + ip_addresses: + - 13.64.39.16/32 + - 40.74.146.80/31 + - 40.74.147.32/28 + tags: + key1: "value1" + state: present + register: results + +- name: Assert that output is not changed + assert: + that: not results.changed + +- name: Update IP group + azure_rm_ipgroup: + resource_group: "{{ resource_group }}" + name: "{{ group_name }}" + location: eastus + ip_addresses: + - 10.0.0.0/24 + tags: + key2: "value2" + register: results + +- name: Assert that IP group is updated + assert: + that: results.changed + +- name: Get IP group facts + azure_rm_ipgroup_info: + resource_group: "{{ resource_group }}" + name: "{{ group_name }}" + register: results + +- assert: + that: + - not results.changed + - results.ipgroups[0].id != None + - results.ipgroups[0].name == "{{ group_name }}" + - results.ipgroups[0].location == "eastus" + - results.ipgroups[0].provisioning_state == "Succeeded" + - results.ipgroups[0].ip_addresses == ["10.0.0.0/24"] + - results.ipgroups[0].tags | length > 0 + +- name: Delete IP group + azure_rm_ipgroup: + resource_group: "{{ resource_group }}" + name: "{{ group_name }}" + state: absent + register: results + +- name: Assert that IP group is deleted + assert: + that: results.changed + +- name: Delete IP group again (Idempotent test) + azure_rm_ipgroup: + resource_group: "{{ resource_group }}" + name: "{{ group_name }}" + state: absent + register: results + +- name: Asset that output is not changed + assert: + that: not results.changed