diff --git a/lib/ansible/module_utils/azure_rm_common_ext.py b/lib/ansible/module_utils/azure_rm_common_ext.py new file mode 100644 index 0000000000..f12b4c9ce1 --- /dev/null +++ b/lib/ansible/module_utils/azure_rm_common_ext.py @@ -0,0 +1,192 @@ +# Copyright (c) 2019 Zim Kalinowski, (@zikalino) +# +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from ansible.module_utils.azure_rm_common import AzureRMModuleBase +import re +from ansible.module_utils.common.dict_transformations import _camel_to_snake, _snake_to_camel +from ansible.module_utils.six import string_types + + +class AzureRMModuleBaseExt(AzureRMModuleBase): + + def inflate_parameters(self, spec, body, level): + if isinstance(body, list): + for item in body: + self.inflate_parameters(spec, item, level) + return + for name in spec.keys(): + # first check if option was passed + param = body.get(name) + if not param: + continue + # check if pattern needs to be used + pattern = spec[name].get('pattern', None) + if pattern: + if pattern == 'camelize': + param = _snake_to_camel(param, True) + else: + param = self.normalize_resource_id(param, pattern) + body[name] = param + disposition = spec[name].get('disposition', '*') + if level == 0 and not disposition.startswith('/'): + continue + if disposition == '/': + disposition = '/*' + parts = disposition.split('/') + if parts[0] == '': + # should fail if level is > 0? + parts.pop(0) + target_dict = body + elem = body.pop(name) + while len(parts) > 1: + target_dict = target_dict.setdefault(parts.pop(0), {}) + targetName = parts[0] if parts[0] != '*' else name + target_dict[targetName] = elem + if spec[name].get('options'): + self.inflate_parameters(spec[name].get('options'), target_dict[targetName], level + 1) + + def normalize_resource_id(self, value, pattern): + ''' + Return a proper resource id string.. + + :param resource_id: It could be a resource name, resource id or dict containing parts from the pattern. + :param pattern: pattern of resource is, just like in Azure Swagger + ''' + value_dict = {} + if isinstance(value, string_types): + value_parts = value.split('/') + if len(value_parts) == 1: + value_dict['name'] = value + else: + pattern_parts = pattern.split('/') + if len(value_parts) != len(pattern_parts): + return None + for i in range(len(value_parts)): + if pattern_parts[i].startswith('{'): + value_dict[pattern_parts[i][1:-1]] = value_parts[i] + elif value_parts[i].lower() != pattern_parts[i].lower(): + return None + elif isinstance(value, dict): + value_dict = value + else: + return None + if not value_dict.get('subscription_id'): + value_dict['subscription_id'] = self.subscription_id + if not value_dict.get('resource_group'): + value_dict['resource_group'] = self.resource_group + + # check if any extra values passed + for k in value_dict: + if not ('{' + k + '}') in pattern: + return None + # format url + return pattern.format(**value_dict) + + def idempotency_check(self, old_params, new_params): + ''' + Return True if something changed. Function will use fields from module_arg_spec to perform dependency checks. + :param old_params: old parameters dictionary, body from Get request. + :param new_params: new parameters dictionary, unpacked module parameters. + ''' + modifiers = {} + result = {} + self.create_compare_modifiers(self.module.argument_spec, '', modifiers) + self.results['modifiers'] = modifiers + return self.default_compare(modifiers, new_params, old_params, '', self.results) + + def create_compare_modifiers(self, arg_spec, path, result): + for k in arg_spec.keys(): + o = arg_spec[k] + updatable = o.get('updatable', True) + comparison = o.get('comparison', 'default') + disposition = o.get('disposition', '*') + if disposition == '/': + disposition = '/*' + p = (path + + ('/' if len(path) > 0 else '') + + disposition.replace('*', k) + + ('/*' if o['type'] == 'list' else '')) + if comparison != 'default' or not updatable: + result[p] = {'updatable': updatable, 'comparison': comparison} + if o.get('options'): + self.create_compare_modifiers(o.get('options'), p, result) + + def default_compare(self, modifiers, new, old, path, result): + ''' + Default dictionary comparison. + This function will work well with most of the Azure resources. + It correctly handles "location" comparison. + + Value handling: + - if "new" value is None, it will be taken from "old" dictionary if "incremental_update" + is enabled. + List handling: + - if list contains "name" field it will be sorted by "name" before comparison is done. + - if module has "incremental_update" set, items missing in the new list will be copied + from the old list + + Warnings: + If field is marked as non-updatable, appropriate warning will be printed out and + "new" structure will be updated to old value. + + :modifiers: Optional dictionary of modifiers, where key is the path and value is dict of modifiers + :param new: New version + :param old: Old version + + Returns True if no difference between structures has been detected. + Returns False if difference was detected. + ''' + if new is None: + return True + elif isinstance(new, dict): + if not isinstance(old, dict): + result['compare'] = 'changed [' + path + '] old dict is null' + return False + for k in new.keys(): + if not self.default_compare(modifiers, new.get(k), old.get(k, None), path + '/' + k, result): + return False + return True + elif isinstance(new, list): + if not isinstance(old, list) or len(new) != len(old): + result['compare'] = 'changed [' + path + '] length is different or null' + return False + if isinstance(old[0], dict): + key = None + if 'id' in old[0] and 'id' in new[0]: + key = 'id' + elif 'name' in old[0] and 'name' in new[0]: + key = 'name' + else: + key = next(iter(old[0])) + new = sorted(new, key=lambda x: x.get(key, None)) + old = sorted(old, key=lambda x: x.get(key, None)) + else: + new = sorted(new) + old = sorted(old) + for i in range(len(new)): + if not self.default_compare(modifiers, new[i], old[i], path + '/*', result): + return False + return True + else: + updatable = modifiers.get(path, {}).get('updatable', True) + comparison = modifiers.get(path, {}).get('comparison', 'default') + if comparison == 'ignore': + return True + elif comparison == 'default' or comparison == 'sensitive': + if isinstance(old, string_types) and isinstance(new, string_types): + new = new.lower() + old = old.lower() + elif comparison == 'location': + if isinstance(old, string_types) and isinstance(new, string_types): + new = new.replace(' ', '').lower() + old = old.replace(' ', '').lower() + if str(new) != str(old): + result['compare'] = 'changed [' + path + '] ' + str(new) + ' != ' + str(old) + ' - ' + str(comparison) + if updatable: + return False + else: + self.module.warn("property '" + path + "' cannot be updated (" + str(old) + "->" + str(new) + ")") + return True + else: + return True diff --git a/lib/ansible/modules/cloud/azure/azure_rm_azurefirewall.py b/lib/ansible/modules/cloud/azure/azure_rm_azurefirewall.py new file mode 100644 index 0000000000..4a72af53a7 --- /dev/null +++ b/lib/ansible/modules/cloud/azure/azure_rm_azurefirewall.py @@ -0,0 +1,716 @@ +#!/usr/bin/python +# +# Copyright (c) 2019 Zim Kalinowski, (@zikalino), Jurijs Fadejevs (@needgithubid) +# +# 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 = ''' +--- +module: azure_rm_azurefirewall +version_added: '2.9' +short_description: Manage Azure Firewall instance. +description: + - 'Create, update and delete instance of Azure Firewall.' +options: + resource_group: + description: + - The name of the resource group. + required: true + name: + description: + - The name of the Azure Firewall. + required: true + location: + description: + - Resource location. + application_rule_collections: + description: + - Collection of application rule collections used by Azure Firewall. + type: list + suboptions: + priority: + description: + - Priority of the application rule collection resource. + type: int + action: + description: + - The action type of a rule collection + choices: + - allow + - deny + rules: + description: + - Collection of rules used by a application rule collection. + type: list + suboptions: + name: + description: + - Name of the application rule. + description: + description: + - Description of the rule. + source_addresses: + description: + - List of source IP addresses for this rule. + type: list + protocols: + description: + - Array of ApplicationRuleProtocols. + type: list + target_fqdns: + description: + - List of FQDNs for this rule. + type: list + fqdn_tags: + description: + - List of FQDN Tags for this rule. + type: list + name: + description: + - >- + Gets name of the resource that is unique within a resource group. + This name can be used to access the resource. + nat_rule_collections: + description: + - Collection of NAT rule collections used by Azure Firewall. + type: list + suboptions: + priority: + description: + - Priority of the NAT rule collection resource. + type: int + action: + description: + - The action type of a NAT rule collection + choices: + - snat + - dnat + rules: + description: + - Collection of rules used by a NAT rule collection. + type: list + suboptions: + name: + description: + - Name of the NAT rule. + description: + description: + - Description of the rule. + source_addresses: + description: + - List of source IP addresses for this rule. + type: list + destination_addresses: + description: + - List of destination IP addresses for this rule. + type: list + destination_ports: + description: + - List of destination ports. + type: list + protocols: + description: + - >- + Array of AzureFirewallNetworkRuleProtocols applicable to this + NAT rule. + type: list + translated_address: + description: + - The translated address for this NAT rule. + translated_port: + description: + - The translated port for this NAT rule. + name: + description: + - >- + Gets name of the resource that is unique within a resource group. + This name can be used to access the resource. + network_rule_collections: + description: + - Collection of network rule collections used by Azure Firewall. + type: list + suboptions: + priority: + description: + - Priority of the network rule collection resource. + type: int + action: + description: + - The action type of a rule collection + choices: + - allow + - deny + rules: + description: + - Collection of rules used by a network rule collection. + type: list + suboptions: + name: + description: + - Name of the network rule. + description: + description: + - Description of the rule. + protocols: + description: + - Array of AzureFirewallNetworkRuleProtocols. + type: list + source_addresses: + description: + - List of source IP addresses for this rule. + type: list + destination_addresses: + description: + - List of destination IP addresses. + type: list + destination_ports: + description: + - List of destination ports. + type: list + name: + description: + - >- + Gets name of the resource that is unique within a resource group. + This name can be used to access the resource. + ip_configurations: + description: + - IP configuration of the Azure Firewall resource. + type: list + suboptions: + subnet: + description: + - Existing subnet. + - It can be a string containing subnet resource id. + - It can be a dictionary containing C(name), C(virtual_network_name) and optionally C(resource_group) . + public_ip_address: + description: + - Existing public IP address + - It can be a string containing resource id. + - It can be a string containing a name in current resource group. + - It can be a dictionary containing C(name) and optionally C(resource_group). + name: + description: + - >- + Name of the resource that is unique within a resource group. This + name can be used to access the resource. + state: + description: + - Assert the state of the AzureFirewall. + - >- + Use C(present) to create or update an AzureFirewall and C(absent) to + delete it. + default: present + choices: + - absent + - present +extends_documentation_fragment: + - azure + - azure_tags +author: + - Zim Kalinowski (@zikalino) + - Jurijs Fadejevs (@needgithubid) + +''' + +EXAMPLES = ''' +- name: Create Azure Firewall + azure_rm_azurefirewall: + resource_group: myResourceGroup + name: myAzureFirewall + tags: + key1: value1 + application_rule_collections: + - priority: 110 + action: + type: deny + rules: + - name: rule1 + description: Deny inbound rule + source_addresses: + - 216.58.216.164 + - 10.0.0.0/24 + protocols: + - type: https + port: '443' + target_fqdns: + - www.test.com + name: apprulecoll + nat_rule_collections: + - priority: 112 + action: + type: dnat + rules: + - name: DNAT-HTTPS-traffic + description: D-NAT all outbound web traffic for inspection + source_addresses: + - '*' + destination_addresses: + - 1.2.3.4 + destination_ports: + - '443' + protocols: + - tcp + translated_address: 1.2.3.5 + translated_port: '8443' + name: natrulecoll + network_rule_collections: + - priority: 112 + action: + type: deny + rules: + - name: L4-traffic + description: Block traffic based on source IPs and ports + protocols: + - tcp + source_addresses: + - 192.168.1.1-192.168.1.12 + - 10.1.4.12-10.1.4.255 + destination_addresses: + - '*' + destination_ports: + - 443-444 + - '8443' + name: netrulecoll + ip_configurations: + - subnet: >- + /subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/myResourceGroup + /providers/Microsoft.Network/virtualNetworks/myVirtualNetwork + /subnets/AzureFirewallSubnet + public_ip_address: >- + /subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/myResourceGroup + /providers/Microsoft.Network/publicIPAddresses/ + myPublicIpAddress + name: azureFirewallIpConfiguration +- name: Delete Azure Firewall + azure_rm_azurefirewall: + resource_group: myResourceGroup + name: myAzureFirewall + state: absent + +''' + +RETURN = ''' +id: + description: + - Resource ID. + returned: always + type: str + sample: /subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/myResourceGroup/providers/Microsoft.Network/azureFirewalls/myAzureFirewall +''' + +import time +import json +import re +from ansible.module_utils.azure_rm_common_ext import AzureRMModuleBaseExt +from ansible.module_utils.azure_rm_common_rest import GenericRestClient +from copy import deepcopy +try: + from msrestazure.azure_exceptions import CloudError +except ImportError: + # This is handled in azure_rm_common + pass + + +class Actions: + NoAction, Create, Update, Delete = range(4) + + +class AzureRMAzureFirewalls(AzureRMModuleBaseExt): + def __init__(self): + self.module_arg_spec = dict( + resource_group=dict( + type='str', + disposition='resource_group_name', + required=True + ), + name=dict( + type='str', + disposition='azure_firewall_name', + required=True + ), + location=dict( + type='str', + updatable=False, + disposition='/', + comparison='location' + ), + application_rule_collections=dict( + type='list', + disposition='/properties/applicationRuleCollections', + options=dict( + priority=dict( + type='int', + disposition='properties/*' + ), + action=dict( + type='str', + choices=['allow', + 'deny'], + disposition='properties/action/type', + pattern='camelize' + ), + rules=dict( + type='list', + disposition='properties/*', + options=dict( + name=dict( + type='str' + ), + description=dict( + type='str' + ), + source_addresses=dict( + type='list', + disposition='sourceAddresses' + ), + protocols=dict( + type='list', + options=dict( + type=dict( + type='str', + disposition='protocolType' + ), + port=dict( + type='str' + ) + ) + ), + target_fqdns=dict( + type='list', + disposition='targetFqdns' + ), + fqdn_tags=dict( + type='list', + disposition='fqdnTags' + ) + ) + ), + name=dict( + type='str' + ) + ) + ), + nat_rule_collections=dict( + type='list', + disposition='/properties/natRuleCollections', + options=dict( + priority=dict( + type='int', + disposition='properties/*' + ), + action=dict( + type='str', + disposition='properties/action/type', + choices=['snat', + 'dnat'], + pattern='camelize' + ), + rules=dict( + type='list', + disposition='properties/*', + options=dict( + name=dict( + type='str' + ), + description=dict( + type='str' + ), + source_addresses=dict( + type='list', + disposition='sourceAddresses' + ), + destination_addresses=dict( + type='list', + disposition='destinationAddresses' + ), + destination_ports=dict( + type='list', + disposition='destinationPorts' + ), + protocols=dict( + type='list' + ), + translated_address=dict( + type='str', + disposition='translatedAddress' + ), + translated_port=dict( + type='str', + disposition='translatedPort' + ) + ) + ), + name=dict( + type='str' + ) + ) + ), + network_rule_collections=dict( + type='list', + disposition='/properties/networkRuleCollections', + options=dict( + priority=dict( + type='int', + disposition='properties/*' + ), + action=dict( + type='str', + choices=['allow', + 'deny'], + disposition='properties/action/type', + pattern='camelize' + ), + rules=dict( + type='list', + disposition='properties/*', + options=dict( + name=dict( + type='str' + ), + description=dict( + type='str' + ), + protocols=dict( + type='list' + ), + source_addresses=dict( + type='list', + disposition='sourceAddresses' + ), + destination_addresses=dict( + type='list', + disposition='destinationAddresses' + ), + destination_ports=dict( + type='list', + disposition='destinationPorts' + ) + ) + ), + name=dict( + type='str' + ) + ) + ), + ip_configurations=dict( + type='list', + disposition='/properties/ipConfigurations', + options=dict( + subnet=dict( + type='raw', + disposition='properties/subnet/id', + pattern=('/subscriptions/{subscription_id}/resourceGroups' + '/{resource_group}/providers/Microsoft.Network' + '/virtualNetworks/{virtual_network_name}/subnets' + '/{name}') + ), + public_ip_address=dict( + type='raw', + disposition='properties/publicIPAddress/id', + pattern=('/subscriptions/{subscription_id}/resourceGroups' + '/{resource_group}/providers/Microsoft.Network' + '/publicIPAddresses/{name}') + ), + name=dict( + type='str' + ) + ) + ), + state=dict( + type='str', + default='present', + choices=['present', 'absent'] + ) + ) + + self.resource_group = None + self.name = None + self.body = {} + + self.results = dict(changed=False) + self.mgmt_client = None + self.state = None + self.url = None + self.status_code = [200, 201, 202] + self.to_do = Actions.NoAction + + self.body = {} + self.query_parameters = {} + self.query_parameters['api-version'] = '2018-11-01' + self.header_parameters = {} + self.header_parameters['Content-Type'] = 'application/json; charset=utf-8' + + super(AzureRMAzureFirewalls, self).__init__(derived_arg_spec=self.module_arg_spec, + supports_check_mode=True, + supports_tags=True) + + def exec_module(self, **kwargs): + for key in list(self.module_arg_spec.keys()): + if hasattr(self, key): + setattr(self, key, kwargs[key]) + elif kwargs[key] is not None: + self.body[key] = kwargs[key] + + self.inflate_parameters(self.module_arg_spec, self.body, 0) + + old_response = None + response = None + + self.mgmt_client = self.get_mgmt_svc_client(GenericRestClient, + base_url=self._cloud_environment.endpoints.resource_manager) + + resource_group = self.get_resource_group(self.resource_group) + + if 'location' not in self.body: + self.body['location'] = resource_group.location + + self.url = ('/subscriptions' + + '/' + self.subscription_id + + '/resourceGroups' + + '/' + self.resource_group + + '/providers' + + '/Microsoft.Network' + + '/azureFirewalls' + + '/' + self.name) + + old_response = self.get_resource() + + if not old_response: + self.log("AzureFirewall instance doesn't exist") + + if self.state == 'absent': + self.log("Old instance didn't exist") + else: + self.to_do = Actions.Create + else: + self.log('AzureFirewall instance already exists') + + if self.state == 'absent': + self.to_do = Actions.Delete + else: + modifiers = {} + self.create_compare_modifiers(self.module_arg_spec, '', modifiers) + self.results['modifiers'] = modifiers + if not self.default_compare(modifiers, self.body, old_response, '', self.results): + self.to_do = Actions.Update + + if (self.to_do == Actions.Create) or (self.to_do == Actions.Update): + self.log('Need to Create / Update the AzureFirewall instance') + + if self.check_mode: + self.results['changed'] = True + return self.results + + response = self.create_update_resource() + + # if not old_response: + self.results['changed'] = True + # else: + # self.results['changed'] = old_response.__ne__(response) + self.log('Creation / Update done') + elif self.to_do == Actions.Delete: + self.log('AzureFirewall instance deleted') + self.results['changed'] = True + + if self.check_mode: + return self.results + + self.delete_resource() + + # make sure instance is actually deleted, for some Azure resources, instance is hanging around + # for some time after deletion -- this should be really fixed in Azure + while self.get_resource(): + time.sleep(20) + else: + self.log('AzureFirewall instance unchanged') + self.results['changed'] = False + response = old_response + + if response: + self.results["id"] = response["id"] + while response['properties']['provisioningState'] == 'Updating': + time.sleep(30) + response = self.get_resource() + + return self.results + + def create_update_resource(self): + # self.log('Creating / Updating the AzureFirewall instance {0}'.format(self.)) + + try: + response = self.mgmt_client.query(self.url, + 'PUT', + self.query_parameters, + self.header_parameters, + self.body, + self.status_code, + 600, + 30) + except CloudError as exc: + self.log('Error attempting to create the AzureFirewall instance.') + self.fail('Error creating the AzureFirewall instance: {0}'.format(str(exc))) + + try: + response = json.loads(response.text) + except Exception: + response = {'text': response.text} + pass + + return response + + def delete_resource(self): + # self.log('Deleting the AzureFirewall instance {0}'.format(self.)) + try: + response = self.mgmt_client.query(self.url, + 'DELETE', + self.query_parameters, + self.header_parameters, + None, + self.status_code, + 600, + 30) + except CloudError as e: + self.log('Error attempting to delete the AzureFirewall instance.') + self.fail('Error deleting the AzureFirewall instance: {0}'.format(str(e))) + + return True + + def get_resource(self): + # self.log('Checking if the AzureFirewall instance {0} is present'.format(self.)) + found = False + try: + response = self.mgmt_client.query(self.url, + 'GET', + self.query_parameters, + self.header_parameters, + None, + self.status_code, + 600, + 30) + response = json.loads(response.text) + found = True + self.log("Response : {0}".format(response)) + # self.log("AzureFirewall instance : {0} found".format(response.name)) + except CloudError as e: + self.log('Did not find the AzureFirewall instance.') + if found is True: + return response + + return False + + +def main(): + AzureRMAzureFirewalls() + + +if __name__ == '__main__': + main() diff --git a/test/integration/targets/azure_rm_azurefirewall/aliases b/test/integration/targets/azure_rm_azurefirewall/aliases new file mode 100644 index 0000000000..aa77c071a8 --- /dev/null +++ b/test/integration/targets/azure_rm_azurefirewall/aliases @@ -0,0 +1,3 @@ +cloud/azure +shippable/azure/group2 +destructive diff --git a/test/integration/targets/azure_rm_azurefirewall/meta/main.yml b/test/integration/targets/azure_rm_azurefirewall/meta/main.yml new file mode 100644 index 0000000000..95e1952f98 --- /dev/null +++ b/test/integration/targets/azure_rm_azurefirewall/meta/main.yml @@ -0,0 +1,2 @@ +dependencies: + - setup_azure diff --git a/test/integration/targets/azure_rm_azurefirewall/tasks/main.yml b/test/integration/targets/azure_rm_azurefirewall/tasks/main.yml new file mode 100644 index 0000000000..c8fdac1352 --- /dev/null +++ b/test/integration/targets/azure_rm_azurefirewall/tasks/main.yml @@ -0,0 +1,248 @@ +- name: Fix resource prefix + set_fact: + virtual_network_name: myVirtualNetwork + subnet_name: AzureFirewallSubnet + public_ipaddress_name: myPublicIpAddress + azure_firewall_name: myFirewall + +- name: Create virtual network + azure_rm_virtualnetwork: + name: "{{ virtual_network_name }}" + address_prefixes_cidr: + - 10.1.0.0/16 + - 172.100.0.0/16 + dns_servers: + - 127.0.0.1 + - 127.0.0.3 + tags: + testing: testing + delete: on-exit + resource_group: "{{ resource_group }}" + +- name: Create subnet + azure_rm_subnet: + name: "{{ subnet_name }}" + virtual_network_name: "{{ virtual_network_name }}" + resource_group: "{{ resource_group }}" + address_prefix_cidr: "10.1.0.0/24" + +- name: Create public IP address + azure_rm_publicipaddress: + resource_group: "{{ resource_group }}" + allocation_method: Static + name: "{{ public_ipaddress_name }}" + sku: Standard + register: pip_output + +- debug: + var: pip_output + +- name: Create Azure Firewall + azure_rm_azurefirewall: + resource_group: '{{resource_group}}' + name: '{{azure_firewall_name}}' + #tags: + # key1: value1 + application_rule_collections: + - priority: 110 + action: deny + rules: + - name: rule1 + description: Deny inbound rule + source_addresses: + - 216.58.216.164 + - 10.0.0.0/25 + protocols: + - type: https + port: '443' + target_fqdns: + - www.test.com + name: apprulecoll + nat_rule_collections: + - priority: 112 + action: dnat + rules: + - name: DNAT-HTTPS-traffic + description: D-NAT all outbound web traffic for inspection + source_addresses: + - '*' + destination_addresses: + - "{{ pip_output.state.ip_address }}" + destination_ports: + - '443' + protocols: + - tcp + translated_address: 1.2.3.5 + translated_port: '8443' + name: natrulecoll + network_rule_collections: + - priority: 112 + action: deny + rules: + - name: L4-traffic + description: Block traffic based on source IPs and ports + protocols: + - tcp + source_addresses: + - 192.168.1.1-192.168.1.12 + - 10.1.4.12-10.1.4.255 + destination_addresses: + - '*' + destination_ports: + - 443-444 + - '8443' + name: netrulecoll + ip_configurations: + - subnet: + virtual_network_name: "{{ virtual_network_name }}" + name: "{{ subnet_name }}" + public_ip_address: + name: "{{ public_ipaddress_name }}" + name: azureFirewallIpConfiguration + register: output + +- debug: + var: output + +- name: Assert that output has changed + assert: + that: + - output.changed + +- name: Create Azure Firewall -- idempotent + azure_rm_azurefirewall: + resource_group: '{{resource_group}}' + name: '{{azure_firewall_name}}' + application_rule_collections: + - priority: 110 + action: deny + rules: + - name: rule1 + description: Deny inbound rule + source_addresses: + - 216.58.216.164 + - 10.0.0.0/25 + protocols: + - type: https + port: '443' + target_fqdns: + - www.test.com + name: apprulecoll + nat_rule_collections: + - priority: 112 + action: dnat + rules: + - name: DNAT-HTTPS-traffic + description: D-NAT all outbound web traffic for inspection + source_addresses: + - '*' + destination_addresses: + - "{{ pip_output.state.ip_address }}" + destination_ports: + - '443' + protocols: + - tcp + translated_address: 1.2.3.5 + translated_port: '8443' + name: natrulecoll + network_rule_collections: + - priority: 112 + action: deny + rules: + - name: L4-traffic + description: Block traffic based on source IPs and ports + protocols: + - tcp + source_addresses: + - 192.168.1.1-192.168.1.12 + - 10.1.4.12-10.1.4.255 + destination_addresses: + - '*' + destination_ports: + - 443-444 + - '8443' + name: netrulecoll + ip_configurations: + - subnet: + virtual_network_name: "{{ virtual_network_name }}" + name: "{{ subnet_name }}" + public_ip_address: + name: "{{ public_ipaddress_name }}" + name: azureFirewallIpConfiguration + register: output + +- debug: + var: output + +- name: Assert that output has not changed + assert: + that: + - not output.changed + +- name: Create Azure Firewall -- change something + azure_rm_azurefirewall: + resource_group: '{{resource_group}}' + name: '{{azure_firewall_name}}' + application_rule_collections: + - priority: 110 + action: deny + rules: + - name: rule1 + description: Deny inbound rule + source_addresses: + - 216.58.216.165 + - 10.0.0.0/25 + protocols: + - type: https + port: '443' + target_fqdns: + - www.test.com + name: apprulecoll + nat_rule_collections: + - priority: 112 + action: dnat + rules: + - name: DNAT-HTTPS-traffic + description: D-NAT all outbound web traffic for inspection + source_addresses: + - '*' + destination_addresses: + - "{{ pip_output.state.ip_address }}" + destination_ports: + - '443' + protocols: + - tcp + translated_address: 1.2.3.6 + translated_port: '8443' + name: natrulecoll + network_rule_collections: + - priority: 112 + action: deny + rules: + - name: L4-traffic + description: Block traffic based on source IPs and ports + protocols: + - tcp + source_addresses: + - 192.168.1.1-192.168.1.12 + - 10.1.4.12-10.1.4.255 + destination_addresses: + - '*' + destination_ports: + - 443-445 + - '8443' + name: netrulecoll + ip_configurations: + - subnet: + virtual_network_name: "{{ virtual_network_name }}" + name: "{{ subnet_name }}" + public_ip_address: + name: "{{ public_ipaddress_name }}" + name: azureFirewallIpConfiguration + check_mode: yes + register: output + +- name: Assert that output has changed + assert: + that: + - output.changed