From 45b239dac538fadbf972d4fffd4da27f14889615 Mon Sep 17 00:00:00 2001 From: Ross Bender Date: Thu, 12 Aug 2021 22:01:37 -0500 Subject: [PATCH] New modules for webapp vnet connection (#590) * info module for webapp vnetintegration * add module for configuring vnet connection * add coverage for webapp vnetconnection module * add doc/examples * add delete idempotent test for webpp vnetconnection * formatting * formatting and logging * Apply suggestions from code review Co-authored-by: Fred-sun <37327967+Fred-sun@users.noreply.github.com> * rename module for collection consistency * correct import location and line lengths * __future__ imports needed at top * add tests for _info module; add to asserts on main module tests * add new module to build/test pipeline * clearer test names * Apply suggestions from code review Co-authored-by: Fred-sun <37327967+Fred-sun@users.noreply.github.com> * Update plugins/modules/azure_rm_webappvnetconnection_info.py Co-authored-by: Fred-sun <37327967+Fred-sun@users.noreply.github.com> Co-authored-by: Fred-sun <37327967+Fred-sun@users.noreply.github.com> --- plugins/module_utils/azure_rm_common.py | 8 + .../modules/azure_rm_webappvnetconnection.py | 259 ++++++++++++++++++ .../azure_rm_webappvnetconnection_info.py | 164 +++++++++++ pr-pipelines.yml | 1 + .../azure_rm_webappvnetconnection/aliases | 3 + .../meta/main.yml | 2 + .../tasks/main.yml | 129 +++++++++ 7 files changed, 566 insertions(+) create mode 100644 plugins/modules/azure_rm_webappvnetconnection.py create mode 100644 plugins/modules/azure_rm_webappvnetconnection_info.py create mode 100644 tests/integration/targets/azure_rm_webappvnetconnection/aliases create mode 100644 tests/integration/targets/azure_rm_webappvnetconnection/meta/main.yml create mode 100644 tests/integration/targets/azure_rm_webappvnetconnection/tasks/main.yml diff --git a/plugins/module_utils/azure_rm_common.py b/plugins/module_utils/azure_rm_common.py index 9c525d487..0bd861409 100644 --- a/plugins/module_utils/azure_rm_common.py +++ b/plugins/module_utils/azure_rm_common.py @@ -966,6 +966,14 @@ def get_data_svc_client(self, **kwags): config = self.add_user_agent(config) return ServiceClient(creds=config.credentials, config=config) + def get_subnet_detail(self, subnet_id): + vnet_detail = subnet_id.split('/Microsoft.Network/virtualNetworks/')[1].split('/subnets/') + return dict( + resource_group=subnet_id.split('resourceGroups/')[1].split('/')[0], + vnet_name=vnet_detail[0], + subnet_name=vnet_detail[1], + ) + # passthru methods to AzureAuth instance for backcompat @property def credentials(self): diff --git a/plugins/modules/azure_rm_webappvnetconnection.py b/plugins/modules/azure_rm_webappvnetconnection.py new file mode 100644 index 000000000..41254ab54 --- /dev/null +++ b/plugins/modules/azure_rm_webappvnetconnection.py @@ -0,0 +1,259 @@ +#!/usr/bin/python +# +# Copyright (c) 2021 Ross Bender (@l3ender) +# +# 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_webappvnetconnection +version_added: "1.8.0" +short_description: Manage web app virtual network connection +description: + - Add, remove, or update the virtual network connection for a web app. +options: + name: + description: + - Name of the web app. + required: true + type: str + resource_group: + description: + - Resource group of the web app. + required: true + type: str + state: + description: + - State of the virtual network connection. Use C(present) to create or update and C(absent) to delete. + type: str + default: present + choices: + - absent + - present + vnet_name: + description: + - Name of the virtual network. Required if adding or updating. + type: str + subnet: + description: + - Name of the virtual network's subnet. Required if adding or updating. + type: str + vnet_resource_group: + description: + - Name of the resource group for the virtual network. Defaults to main C(resource_group) value. + type: str + +extends_documentation_fragment: + - azure.azcollection.azure + +author: + - Ross Bender (@l3ender) +''' + +EXAMPLES = ''' + - name: Configure web app with virtual network + azure.azcollection.azure_rm_webappvnetconnection: + name: "MyWebapp" + resource_group: "MyResourceGroup" + vnet_name: "MyVnetName" + subnet: "MySubnetName" + + - name: Configure web app with virtual network in different resource group + azure.azcollection.azure_rm_webappvnetconnection: + name: "MyWebapp" + resource_group: "MyResourceGroup" + vnet_name: "MyVnetName" + subnet: "MySubnetName" + vnet_resource_group: "MyOtherResourceGroup" + + - name: Delete web app virtual network + azure.azcollection.azure_rm_webappvnetconnection: + name: "MyWebapp" + resource_group: "MyResourceGroup" + state: "absent" +''' + +RETURN = ''' +connection: + description: + - The web app's virtual network connection. + returned: always + type: complex + contains: + id: + description: + - ID of the web app virtual network connection. + returned: always + type: str + sample: /subscriptions/xxx-xxx/resourceGroups/myResourceGroup/providers/Microsoft.Web/sites/myWebApp/virtualNetworkConnections/yyy-yyy_subnet + name: + description: + - Name of the web app virtual network connection. + returned: always + type: str + sample: yyy-yyy_subnet + subnet_name: + description: + - Name of the subnet connected to the web app. + returned: always + type: str + sample: mySubnet + vnet_name: + description: + - Name of the virtual network connected to the web app. + returned: always + type: str + sample: myVnet + vnet_resource_group: + description: + - Name of the resource group the virtual network is in. + returned: always + type: str + sample: myResourceGroup + vnet_resource_id: + description: + - ID of the virtual network/subnet connected to the web app. + returned: always + type: str + sample: /subscriptions/xxx-xxx/resourceGroups/myResourceGroup/providers/Microsoft.Network/virtualNetworks/myVnet/subnets/mySubnet +''' + +from ansible_collections.azure.azcollection.plugins.module_utils.azure_rm_common import AzureRMModuleBase + +try: + from azure.mgmt.web.models import SwiftVirtualNetwork +except Exception: + # This is handled in azure_rm_common + pass + + +class AzureRMWebAppVnetConnection(AzureRMModuleBase): + + def __init__(self): + + self.module_arg_spec = dict( + name=dict(type='str', required=True), + resource_group=dict(type='str', required=True), + state=dict(type='str', default='present', choices=['present', 'absent']), + vnet_name=dict(type='str'), + subnet=dict(type='str'), + vnet_resource_group=dict(type='str'), + ) + + self.results = dict( + changed=False, + connection=dict(), + ) + + self.state = None + self.name = None + self.resource_group = None + self.vnet_name = None + self.subnet = None + self.vnet_resource_group = None + + super(AzureRMWebAppVnetConnection, self).__init__(self.module_arg_spec, + supports_check_mode=True, + supports_tags=False) + + def exec_module(self, **kwargs): + for key in self.module_arg_spec: + setattr(self, key, kwargs[key]) + + changed = False + vnet = self.get_vnet_connection() + if vnet: + self.results['connection'] = self.set_results(vnet) + + if self.state == 'absent' and vnet: + changed = True + if not self.check_mode: + self.log('Deleting vnet connection for webapp {0}'.format(self.name)) + self.delete_vnet_connection() + self.results['connection'] = dict() + elif self.state == 'present': + self.vnet_resource_group = self.vnet_resource_group or self.resource_group + + if not vnet: + self.log('Adding vnet connection for webapp {0}'.format(self.name)) + changed = True + else: + subnet_detail = self.get_subnet_detail(vnet.vnet_resource_id) + if (subnet_detail['resource_group'] != self.vnet_resource_group + or subnet_detail['vnet_name'] != self.vnet_name + or subnet_detail['subnet_name'] != self.subnet): + self.log('Detected change in vnet connection for webapp {0}'.format(self.name)) + changed = True + + if changed: + if not self.check_mode: + self.log('Updating vnet connection for webapp {0}'.format(self.name)) + subnet = self.get_subnet() + param = SwiftVirtualNetwork(subnet_resource_id=subnet.id) + self.create_or_update_vnet_connection(param) + vnet = self.get_vnet_connection() + self.results['connection'] = self.set_results(vnet) + + self.results['changed'] = changed + return self.results + + def get_vnet_connection(self): + connections = self.list_vnet_connections() + for connection in connections: + if connection.is_swift: + return connection + + return None + + def list_vnet_connections(self): + try: + return self.web_client.web_apps.list_vnet_connections(resource_group_name=self.resource_group, name=self.name) + except Exception as exc: + self.fail("Error getting webapp vnet connections {0} (rg={1}) - {2}".format(self.name, self.resource_group, str(exc))) + + def delete_vnet_connection(self): + try: + return self.web_client.web_apps.delete_swift_virtual_network(resource_group_name=self.resource_group, name=self.name) + except Exception as exc: + self.fail("Error deleting webapp vnet connection {0} (rg={1}) - {3}".format(self.name, self.resource_group, str(exc))) + + def create_or_update_vnet_connection(self, vnet): + try: + return self.web_client.web_apps.create_or_update_swift_virtual_network_connection( + resource_group_name=self.resource_group, name=self.name, connection_envelope=vnet) + except Exception as exc: + self.fail("Error creating/updating webapp vnet connection {0} (vnet={1}, rg={2}) - {3}".format( + self.name, self.vnet_name, self.resource_group, str(exc))) + + def get_subnet(self): + try: + return self.network_client.subnets.get(resource_group_name=self.vnet_resource_group, virtual_network_name=self.vnet_name, subnet_name=self.subnet) + except Exception as exc: + self.fail("Error getting subnet {0} in vnet={1} (rg={2}) - {3}".format(self.subnet, self.vnet_name, self.vnet_resource_group, str(exc))) + + def set_results(self, vnet): + vnet_dict = vnet.as_dict() + + output = dict() + output['id'] = vnet_dict['id'] + output['name'] = vnet_dict['name'] + subnet_id = vnet_dict.get('subnet_resource_id', vnet_dict.get('vnet_resource_id')) + output['vnet_resource_id'] = subnet_id + subnet_detail = self.get_subnet_detail(subnet_id) + output['vnet_resource_group'] = subnet_detail['resource_group'] + output['vnet_name'] = subnet_detail['vnet_name'] + output['subnet_name'] = subnet_detail['subnet_name'] + + return output + + +def main(): + AzureRMWebAppVnetConnection() + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/azure_rm_webappvnetconnection_info.py b/plugins/modules/azure_rm_webappvnetconnection_info.py new file mode 100644 index 000000000..8ad772918 --- /dev/null +++ b/plugins/modules/azure_rm_webappvnetconnection_info.py @@ -0,0 +1,164 @@ +#!/usr/bin/python +# +# Copyright (c) 2021 Ross Bender (@l3ender) +# +# 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_webappvnetconnection_info + +version_added: "1.8.0" + +short_description: Get Azure web app virtual network connection facts + +description: + - Get facts for a web app's virtual network connection. + +options: + name: + description: + - Name of the web app. + required: true + type: str + resource_group: + description: + - Resource group of the web app. + required: true + type: str + +extends_documentation_fragment: + - azure.azcollection.azure + +author: + - Ross Bender (@l3ender) +''' + +EXAMPLES = ''' + - name: Get web app virtual network connection + azure_rm_webappvnetconnection_info: + name: "MyWebapp" + resource_group: "MyResourceGroup" +''' + +RETURN = ''' +connection: + description: + - The web app's virtual network connection. + returned: always + type: complex + contains: + id: + description: + - ID of the web app virtual network connection. + returned: always + type: str + sample: /subscriptions/xxx-xxx/resourceGroups/myResourceGroup/providers/Microsoft.Web/sites/myWebApp/virtualNetworkConnections/yyy-yyy_subnet + name: + description: + - Name of the web app virtual network connection. + returned: always + type: str + sample: yyy-yyy_subnet + subnet_name: + description: + - Name of the subnet connected to the web app. + returned: always + type: str + sample: mySubnet + vnet_name: + description: + - Name of the virtual network connected to the web app. + returned: always + type: str + sample: myVnet + vnet_resource_group: + description: + - Name of the resource group the virtual network is in. + returned: always + type: str + sample: myResourceGroup + vnet_resource_id: + description: + - ID of the virtual network/subnet connected to the web app. + returned: always + type: str + sample: /subscriptions/xxx-xxx/resourceGroups/myResourceGroup/providers/Microsoft.Network/virtualNetworks/myVnet/subnets/mySubnet +''' + +from ansible_collections.azure.azcollection.plugins.module_utils.azure_rm_common import AzureRMModuleBase + + +class AzureRMWebAppVnetConnectionInfo(AzureRMModuleBase): + + def __init__(self): + + self.module_arg_spec = dict( + name=dict(type='str', required=True), + resource_group=dict(type='str', required=True), + ) + + self.results = dict( + changed=False, + connection=dict(), + ) + + self.name = None + self.resource_group = None + + super(AzureRMWebAppVnetConnectionInfo, self).__init__(self.module_arg_spec, + supports_check_mode=True, + supports_tags=False, + facts_module=True) + + def exec_module(self, **kwargs): + for key in self.module_arg_spec: + setattr(self, key, kwargs[key]) + + vnet = self.get_vnet_connection() + + if vnet: + self.results['connection'] = self.set_results(vnet) + + return self.results + + def get_vnet_connection(self): + connections = self.list_vnet_connections() + for connection in connections: + if connection.is_swift: + return connection + + return None + + def list_vnet_connections(self): + try: + return self.web_client.web_apps.list_vnet_connections(resource_group_name=self.resource_group, name=self.name) + except Exception as exc: + self.fail("Error getting webapp vnet connections {0} (rg={1}) - {2}".format(self.name, self.resource_group, str(exc))) + + def set_results(self, vnet): + vnet_dict = vnet.as_dict() + + output = dict() + output['id'] = vnet_dict['id'] + output['name'] = vnet_dict['name'] + subnet_id = vnet_dict['vnet_resource_id'] + output['vnet_resource_id'] = subnet_id + subnet_detail = self.get_subnet_detail(subnet_id) + output['vnet_resource_group'] = subnet_detail['resource_group'] + output['vnet_name'] = subnet_detail['vnet_name'] + output['subnet_name'] = subnet_detail['subnet_name'] + + return output + + +def main(): + AzureRMWebAppVnetConnectionInfo() + + +if __name__ == '__main__': + main() diff --git a/pr-pipelines.yml b/pr-pipelines.yml index e04c32013..41a5036a6 100644 --- a/pr-pipelines.yml +++ b/pr-pipelines.yml @@ -105,6 +105,7 @@ parameters: - "azure_rm_recoveryservicesvault" - "azure_rm_vmbackuppolicy" - "azure_rm_webapp" + - "azure_rm_webappvnetconnection" - "azure_rm_webappaccessrestriction" - "azure_rm_workspace" - "inventory_azure" diff --git a/tests/integration/targets/azure_rm_webappvnetconnection/aliases b/tests/integration/targets/azure_rm_webappvnetconnection/aliases new file mode 100644 index 000000000..759eafa2d --- /dev/null +++ b/tests/integration/targets/azure_rm_webappvnetconnection/aliases @@ -0,0 +1,3 @@ +cloud/azure +shippable/azure/group3 +destructive diff --git a/tests/integration/targets/azure_rm_webappvnetconnection/meta/main.yml b/tests/integration/targets/azure_rm_webappvnetconnection/meta/main.yml new file mode 100644 index 000000000..95e1952f9 --- /dev/null +++ b/tests/integration/targets/azure_rm_webappvnetconnection/meta/main.yml @@ -0,0 +1,2 @@ +dependencies: + - setup_azure diff --git a/tests/integration/targets/azure_rm_webappvnetconnection/tasks/main.yml b/tests/integration/targets/azure_rm_webappvnetconnection/tasks/main.yml new file mode 100644 index 000000000..66a1b55cf --- /dev/null +++ b/tests/integration/targets/azure_rm_webappvnetconnection/tasks/main.yml @@ -0,0 +1,129 @@ +- name: Prepare random number + set_fact: + rpfx: "{{ resource_group | hash('md5') | truncate(7, True, '') }}{{ 1000 | random }}" + run_once: true + +- name: Create a virtual network + azure_rm_virtualnetwork: + name: vnet{{ rpfx }} + resource_group: "{{ resource_group }}" + address_prefixes_cidr: + - 10.1.0.0/16 + - 172.100.0.0/16 + dns_servers: + - 127.0.0.1 + - 127.0.0.2 +- name: Create a subnet + azure_rm_subnet: + name: subnet{{ rpfx }} + virtual_network_name: vnet{{ rpfx }} + resource_group: "{{ resource_group }}" + address_prefix_cidr: 10.1.0.0/24 + delegations: + - name: 'mydeleg' + serviceName: 'Microsoft.Web/serverFarms' + register: subnet_output +- name: Create a web app + azure_rm_webapp: + resource_group: "{{ resource_group }}" + name: webapp{{ rpfx }} + plan: + resource_group: "{{ resource_group }}" + name: webappplan{{ rpfx }} + is_linux: false + sku: S1 + +- name: "Create webapp vnetconnection - check mode" + azure_rm_webappvnetconnection: + name: webapp{{ rpfx }} + resource_group: "{{ resource_group }}" + vnet_name: vnet{{ rpfx }} + subnet: subnet{{ rpfx }} + check_mode: true + register: output +- name: Assert the resource is well created + assert: + that: output.changed + +- name: "Check webapp vnetconnection facts 1" + azure_rm_webappvnetconnection_info: + name: webapp{{ rpfx }} + resource_group: "{{ resource_group }}" + register: output +- name: Assert the resource has no connections + assert: + that: + - not output.changed + - output.connection | length == 0 + +- name: "Create webapp vnetconnection" + azure_rm_webappvnetconnection: + name: webapp{{ rpfx }} + resource_group: "{{ resource_group }}" + vnet_name: vnet{{ rpfx }} + subnet: subnet{{ rpfx }} + register: output +- name: Assert the resource is well created + assert: + that: + - output.changed + - output.connection.vnet_name == 'vnet{{ rpfx }}' + - output.connection.subnet_name == 'subnet{{ rpfx }}' + - output.connection.vnet_resource_group == '{{ resource_group }}' + +- name: "Check webapp vnetconnection facts 2" + azure_rm_webappvnetconnection_info: + name: webapp{{ rpfx }} + resource_group: "{{ resource_group }}" + register: output +- name: Assert the connection exists + assert: + that: + - not output.changed + - output.connection.vnet_name == 'vnet{{ rpfx }}' + - output.connection.subnet_name == 'subnet{{ rpfx }}' + - output.connection.vnet_resource_group == '{{ resource_group }}' + +- name: "Create webapp vnetconnection - idempotent" + azure_rm_webappvnetconnection: + name: webapp{{ rpfx }} + resource_group: "{{ resource_group }}" + vnet_name: vnet{{ rpfx }} + subnet: subnet{{ rpfx }} + register: output +- name: Assert the resource is not changed + assert: + that: not output.changed + +- name: "Delete webapp vnetconnection" + azure_rm_webappvnetconnection: + name: webapp{{ rpfx }} + resource_group: "{{ resource_group }}" + state: "absent" + register: output +- name: Assert the connection is deleted + assert: + that: + - output.changed + - output.connection | length == 0 + +- name: "Check webapp vnetconnection facts 3" + azure_rm_webappvnetconnection_info: + name: webapp{{ rpfx }} + resource_group: "{{ resource_group }}" + register: output +- name: Assert the resource has no connections + assert: + that: + - not output.changed + - output.connection | length == 0 + +- name: "Delete webapp vnetconnection - idempotent" + azure_rm_webappvnetconnection: + name: webapp{{ rpfx }} + resource_group: "{{ resource_group }}" + state: "absent" + register: output +- name: Assert the resource is not changed + assert: + that: not output.changed