#!/usr/bin/python # # Copyright (c) 2016 Matt Davis, # Chris Houseknecht, # # This file is part of Ansible # # Ansible is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # Ansible is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . # DOCUMENTATION = ''' --- module: azure_rm_securitygroup version_added: "2.1" short_description: Manage Azure network security groups. description: - Create, update or delete a network security group. A security group contains Access Control List (ACL) rules that allow or deny network traffic to subnets or individual network interfaces. A security group is created with a set of default security rules and an empty set of security rules. Shape traffic flow by adding rules to the empty set of security rules. options: default_rules: description: - The set of default rules automatically added to a security group at creation. In general default rules will not be modified. Modify rules to shape the flow of traffic to or from a subnet or NIC. See rules below for the makeup of a rule dict. required: false default: null location: description: - Valid azure location. Defaults to location of the resource group. default: resource_group location required: false name: description: - Name of the security group to operate on. required: false default: null purge_default_rules: description: - Remove any existing rules not matching those defined in the default_rules parameter. default: false required: false purge_rules: description: - Remove any existing rules not matching those defined in the rules parameters. default: false required: false resource_group: description: - Name of the resource group the security group belongs to. required: true rules: description: - Set of rules shaping traffic flow to or from a subnet or NIC. Each rule is a dictionary. type: complex required: false default: null contains: name: description: Unique name for the rule. required: true description: description: Short description of the rule's purpose. protocol: description: Accepted traffic protocol. choices: - Udp - Tcp - "*" default: "*" source_port_range: description: Port or range of ports from which traffic originates. default: "*" destination_port_range: description: Port or range of ports to which traffic is headed. default: "*" source_address_prefix: description: IP address or CIDR from which traffic originates. default: "*" destination_address_prefix: description: IP address or CIDR to which traffic is headed. default: "*" access: description: Whether or not to allow the traffic flow. choices: - Allow - Deny default: Allow priority: description: Order in which to apply the rule. Must a unique integer between 100 and 4096 inclusive. type: int required: true direction: description: Indicates the direction of the traffic flow. choices: - Inbound - Outbound default: Inbound state: description: - Assert the state of the security group. Set to 'present' to create or update a security group. Set to 'absent' to remove a security group. default: present required: false choices: - absent - present extends_documentation_fragment: - azure - azure_tags author: - "Chris Houseknecht (@chouseknecht)" - "Matt Davis (@nitzmahone)" ''' EXAMPLES = ''' # Create a security group - azure_rm_securitygroup: resource_group: mygroup name: mysecgroup purge_rules: yes rules: - name: DenySSH protocol: TCP destination_port_range: 22 access: Deny priority: 100 direction: Inbound - name: 'AllowSSH' protocol: TCP source_address_prefix: '174.109.158.0/24' destination_port_range: 22 access: Allow priority: 101 direction: Inbound # Update rules on existing security group - azure_rm_securitygroup: resource_group: mygroup name: mysecgroup rules: - name: DenySSH protocol: TCP destination_port_range: 22-23 access: Deny priority: 100 direction: Inbound - name: AllowSSHFromHome protocol: TCP source_address_prefix: '174.109.158.0/24' destination_port_range: 22-23 access: Allow priority: 102 direction: Inbound tags: testing: testing delete: on-exit # Delete security group - azure_rm_securitygroup: resource_group: mygroup name: mysecgroup state: absent ''' RETURN = ''' state: description: Current state of the security group. returned: always type: dict sample: { "default_rules": [ { "access": "Allow", "description": "Allow inbound traffic from all VMs in VNET", "destination_address_prefix": "VirtualNetwork", "destination_port_range": "*", "direction": "Inbound", "etag": 'W/"edf48d56-b315-40ca-a85d-dbcb47f2da7d"', "id": "/subscriptions/3f7e29ba-24e0-42f6-8d9c-5149a14bda37/resourceGroups/Testing/providers/Microsoft.Network/networkSecurityGroups/mysecgroup/defaultSecurityRules/AllowVnetInBound", "name": "AllowVnetInBound", "priority": 65000, "protocol": "*", "provisioning_state": "Succeeded", "source_address_prefix": "VirtualNetwork", "source_port_range": "*" }, { "access": "Allow", "description": "Allow inbound traffic from azure load balancer", "destination_address_prefix": "*", "destination_port_range": "*", "direction": "Inbound", "etag": 'W/"edf48d56-b315-40ca-a85d-dbcb47f2da7d"', "id": "/subscriptions/3f7e29ba-24e0-42f6-8d9c-5149a14bda37/resourceGroups/Testing/providers/Microsoft.Network/networkSecurityGroups/mysecgroup/defaultSecurityRules/AllowAzureLoadBalancerInBound", "name": "AllowAzureLoadBalancerInBound", "priority": 65001, "protocol": "*", "provisioning_state": "Succeeded", "source_address_prefix": "AzureLoadBalancer", "source_port_range": "*" }, { "access": "Deny", "description": "Deny all inbound traffic", "destination_address_prefix": "*", "destination_port_range": "*", "direction": "Inbound", "etag": 'W/"edf48d56-b315-40ca-a85d-dbcb47f2da7d"', "id": "/subscriptions/3f7e29ba-24e0-42f6-8d9c-5149a14bda37/resourceGroups/Testing/providers/Microsoft.Network/networkSecurityGroups/mysecgroup/defaultSecurityRules/DenyAllInBound", "name": "DenyAllInBound", "priority": 65500, "protocol": "*", "provisioning_state": "Succeeded", "source_address_prefix": "*", "source_port_range": "*" }, { "access": "Allow", "description": "Allow outbound traffic from all VMs to all VMs in VNET", "destination_address_prefix": "VirtualNetwork", "destination_port_range": "*", "direction": "Outbound", "etag": 'W/"edf48d56-b315-40ca-a85d-dbcb47f2da7d"', "id": "/subscriptions/3f7e29ba-24e0-42f6-8d9c-5149a14bda37/resourceGroups/Testing/providers/Microsoft.Network/networkSecurityGroups/mysecgroup/defaultSecurityRules/AllowVnetOutBound", "name": "AllowVnetOutBound", "priority": 65000, "protocol": "*", "provisioning_state": "Succeeded", "source_address_prefix": "VirtualNetwork", "source_port_range": "*" }, { "access": "Allow", "description": "Allow outbound traffic from all VMs to Internet", "destination_address_prefix": "Internet", "destination_port_range": "*", "direction": "Outbound", "etag": 'W/"edf48d56-b315-40ca-a85d-dbcb47f2da7d"', "id": "/subscriptions/3f7e29ba-24e0-42f6-8d9c-5149a14bda37/resourceGroups/Testing/providers/Microsoft.Network/networkSecurityGroups/mysecgroup/defaultSecurityRules/AllowInternetOutBound", "name": "AllowInternetOutBound", "priority": 65001, "protocol": "*", "provisioning_state": "Succeeded", "source_address_prefix": "*", "source_port_range": "*" }, { "access": "Deny", "description": "Deny all outbound traffic", "destination_address_prefix": "*", "destination_port_range": "*", "direction": "Outbound", "etag": 'W/"edf48d56-b315-40ca-a85d-dbcb47f2da7d"', "id": "/subscriptions/3f7e29ba-24e0-42f6-8d9c-5149a14bda37/resourceGroups/Testing/providers/Microsoft.Network/networkSecurityGroups/mysecgroup/defaultSecurityRules/DenyAllOutBound", "name": "DenyAllOutBound", "priority": 65500, "protocol": "*", "provisioning_state": "Succeeded", "source_address_prefix": "*", "source_port_range": "*" } ], "id": "/subscriptions/3f7e29ba-24e0-42f6-8d9c-5149a14bda37/resourceGroups/Testing/providers/Microsoft.Network/networkSecurityGroups/mysecgroup", "location": "westus", "name": "mysecgroup", "network_interfaces": [], "rules": [ { "access": "Deny", "description": null, "destination_address_prefix": "*", "destination_port_range": "22", "direction": "Inbound", "etag": 'W/"edf48d56-b315-40ca-a85d-dbcb47f2da7d"', "id": "/subscriptions/3f7e29ba-24e0-42f6-8d9c-5149a14bda37/resourceGroups/Testing/providers/Microsoft.Network/networkSecurityGroups/mysecgroup/securityRules/DenySSH", "name": "DenySSH", "priority": 100, "protocol": "Tcp", "provisioning_state": "Succeeded", "source_address_prefix": "*", "source_port_range": "*" }, { "access": "Allow", "description": null, "destination_address_prefix": "*", "destination_port_range": "22", "direction": "Inbound", "etag": 'W/"edf48d56-b315-40ca-a85d-dbcb47f2da7d"', "id": "/subscriptions/3f7e29ba-24e0-42f6-8d9c-5149a14bda37/resourceGroups/Testing/providers/Microsoft.Network/networkSecurityGroups/mysecgroup/securityRules/AllowSSH", "name": "AllowSSH", "priority": 101, "protocol": "Tcp", "provisioning_state": "Succeeded", "source_address_prefix": "174.109.158.0/24", "source_port_range": "*" } ], "subnets": [], "tags": { "delete": "on-exit", "foo": "bar", "testing": "testing" }, "type": "Microsoft.Network/networkSecurityGroups" } ''' from ansible.module_utils.basic import * from ansible.module_utils.azure_rm_common import * try: from msrestazure.azure_exceptions import CloudError from azure.common import AzureHttpError from azure.mgmt.network.models import NetworkSecurityGroup, SecurityRule from azure.mgmt.network.models.network_management_client_enums import (SecurityRuleAccess, SecurityRuleDirection, SecurityRuleProtocol) except ImportError: # This is handled in azure_rm_common pass NAME_PATTERN = re.compile(r"^[a-zA-Z0-9._-]+$") def validate_rule(rule, rule_type=None): ''' Apply defaults to a rule dictionary and check that all values are valid. :param rule: rule dict :param rule_type: Set to 'default' if the rule is part of the default set of rules. :return: None ''' if not rule.get('name'): raise Exception("Rule name value is required.") if not NAME_PATTERN.match(rule.get('name')): raise Exception("Rule name must contain only word characters plus '.','-','_'") priority = rule.get('priority', None) if not priority: raise Exception("Rule priority is required.") if not isinstance(priority, (int, long)): raise Exception("Rule priority attribute must be an integer.") if rule_type != 'default' and (priority < 100 or priority > 4096): raise Exception("Rule priority must be between 100 and 4096") if not rule.get('access'): rule['access'] = 'Allow' access_names = [member.value for member in SecurityRuleAccess] if rule['access'] not in access_names: raise Exception("Rule access must be one of [{0}]".format(', '.join(access_names))) if not rule.get('destination_address_prefix'): rule['destination_address_prefix'] = '*' if not rule.get('source_address_prefix'): rule['source_address_prefix'] = '*' if not rule.get('protocol'): rule['protocol'] = '*' protocol_names = [member.value for member in SecurityRuleProtocol] if rule['protocol'] not in protocol_names: raise Exception("Rule protocol must be one of [{0}]".format(', '.join(protocol_names))) if not rule.get('direction'): rule['direction'] = 'Inbound' direction_names = [member.value for member in SecurityRuleDirection] if rule['direction'] not in direction_names: raise Exception("Rule direction must be one of [{0}]".format(', '.join(direction_names))) if not rule.get('source_port_range'): rule['source_port_range'] = '*' if not rule.get('destination_port_range'): rule['destination_port_range'] = '*' def compare_rules(r, rule): matched = False changed = False if r['name'] == rule['name']: matched = True if rule.get('description', None) != r['description']: changed = True r['description'] = rule['description'] if rule['protocol'] != r['protocol']: changed = True r['protocol'] = rule['protocol'] if rule['source_port_range'] != r['source_port_range']: changed = True r['source_port_range'] = rule['source_port_range'] if rule['destination_port_range'] != r['destination_port_range']: changed = True r['destination_port_range'] = rule['destination_port_range'] if rule['access'] != r['access']: changed = True r['access'] = rule['access'] if rule['priority'] != r['priority']: changed = True r['priority'] = rule['priority'] if rule['direction'] != r['direction']: changed = True r['direction'] = rule['direction'] return matched, changed def create_rule_instance(rule): ''' Create an instance of SecurityRule from a dict. :param rule: dict :return: SecurityRule ''' return SecurityRule( rule['protocol'], rule['source_address_prefix'], rule['destination_address_prefix'], rule['access'], rule['direction'], id=rule.get('id', None), description=rule.get('description', None), source_port_range=rule.get('source_port_range', None), destination_port_range=rule.get('destination_port_range', None), priority=rule.get('priority', None), provisioning_state=rule.get('provisioning_state', None), name=rule.get('name', None), etag=rule.get('etag', None) ) def create_rule_dict_from_obj(rule): ''' Create a dict from an instance of a SecurityRule. :param rule: SecurityRule :return: dict ''' return dict( id=rule.id, name=rule.name, description=rule.description, protocol=rule.protocol.value, source_port_range=rule.source_port_range, destination_port_range=rule.destination_port_range, source_address_prefix=rule.source_address_prefix, destination_address_prefix=rule.destination_address_prefix, access=rule.access.value, priority=rule.priority, direction=rule.direction.value, provisioning_state=rule.provisioning_state, etag=rule.etag ) def create_network_security_group_dict(nsg): results = dict( id=nsg.id, name=nsg.name, type=nsg.type, location=nsg.location, tags=nsg.tags, ) results['rules'] = [] if nsg.security_rules: for rule in nsg.security_rules: results['rules'].append(create_rule_dict_from_obj(rule)) results['default_rules'] = [] if nsg.default_security_rules: for rule in nsg.default_security_rules: results['default_rules'].append(create_rule_dict_from_obj(rule)) results['network_interfaces'] = [] if nsg.network_interfaces: for interface in nsg.network_interfaces: results['network_interfaces'].append(interface.id) results['subnets'] = [] if nsg.subnets: for subnet in nsg.subnets: results['subnets'].append(subnet.id) return results class AzureRMSecurityGroup(AzureRMModuleBase): def __init__(self): self.module_arg_spec = dict( default_rules=dict(type='list'), location=dict(type='str'), name=dict(type='str', required=True), purge_default_rules=dict(type='bool', default=False), purge_rules=dict(type='bool', default=False), resource_group=dict(required=True, type='str'), rules=dict(type='list'), state=dict(type='str', default='present', choices=['present', 'absent']), ) self.default_rules = None self.location = None self.name = None self.purge_default_rules = None self.purge_rules = None self.resource_group = None self.rules = None self.state = None self.tags = None self.results = dict( changed=False, state=dict() ) super(AzureRMSecurityGroup, self).__init__(self.module_arg_spec, supports_check_mode=True) def exec_module(self, **kwargs): for key in self.module_arg_spec.keys() + ['tags']: setattr(self, key, kwargs[key]) changed = False results = dict() resource_group = self.get_resource_group(self.resource_group) if not self.location: # Set default location self.location = resource_group.location if not NAME_PATTERN.match(self.name): self.fail("Parameter error: name must contain only word characters and '.','-','_'") if self.rules: for rule in self.rules: try: validate_rule(rule) except Exception as exc: self.fail("Error validating rule {0} - {1}".format(rule, str(exc))) if self.default_rules: for rule in self.default_rules: try: validate_rule(rule, 'default') except Exception as exc: self.fail("Error validating default rule {0} - {1}".format(rule, str(exc))) try: nsg = self.network_client.network_security_groups.get(self.resource_group, self.name) results = create_network_security_group_dict(nsg) self.log("Found security group:") self.log(results, pretty_print=True) self.check_provisioning_state(nsg, self.state) if self.state == 'present': pass elif self.state == 'absent': self.log("CHANGED: security group found but state is 'absent'") changed = True except CloudError: if self.state == 'present': self.log("CHANGED: security group not found and state is 'present'") changed = True if self.state == 'present' and not changed: # update the security group self.log("Update security group {0}".format(self.name)) if self.rules: for rule in self.rules: rule_matched = False for r in results['rules']: match, changed = compare_rules(r, rule) if changed: changed = True if match: rule_matched = True if not rule_matched: changed = True results['rules'].append(rule) if self.purge_rules: new_rules = [] for rule in results['rules']: for r in self.rules: if rule['name'] == r['name']: new_rules.append(rule) results['rules'] = new_rules if self.default_rules: for rule in self.default_rules: rule_matched = False for r in results['default_rules']: match, changed = compare_rules(r, rule) if changed: changed = True if match: rule_matched = True if not rule_matched: changed = True results['default_rules'].append(rule) if self.purge_default_rules: new_default_rules = [] for rule in results['default_rules']: for r in self.default_rules: if rule['name'] == r['name']: new_default_rules.append(rule) results['default_rules'] = new_default_rules update_tags, results['tags'] = self.update_tags(results['tags']) if update_tags: changed = True self.results['changed'] = changed self.results['state'] = results if not self.check_mode: self.results['state'] = self.create_or_update(results) elif self.state == 'present' and changed: # create the security group self.log("Create security group {0}".format(self.name)) if not self.location: self.fail("Parameter error: location required when creating a security group.") results['name'] = self.name results['location'] = self.location results['rules'] = [] results['default_rules'] = [] results['tags'] = {} if self.rules: results['rules'] = self.rules if self.default_rules: results['default_rules'] = self.default_rules if self.tags: results['tags'] = self.tags self.results['changed'] = changed self.results['state'] = results if not self.check_mode: self.results['state'] = self.create_or_update(results) elif self.state == 'absent' and changed: self.log("Delete security group {0}".format(self.name)) self.results['changed'] = changed self.results['state'] = dict() if not self.check_mode: self.delete() # the delete does not actually return anything. if no exception, then we'll assume # it worked. self.results['state']['status'] = 'Deleted' return self.results def create_or_update(self, results): parameters = NetworkSecurityGroup() if results.get('rules'): parameters.security_rules = [] for rule in results.get('rules'): parameters.security_rules.append(create_rule_instance(rule)) if results.get('default_rules'): parameters.default_security_rules = [] for rule in results.get('default_rules'): parameters.default_security_rules.append(create_rule_instance(rule)) parameters.tags = results.get('tags') parameters.location = results.get('location') try: poller = self.network_client.network_security_groups.create_or_update(self.resource_group, self.name, parameters) result = self.get_poller_result(poller) except AzureHttpError as exc: self.fail("Error creating/upating security group {0} - {1}".format(self.name, str(exc))) return create_network_security_group_dict(result) def delete(self): try: poller = self.network_client.network_security_groups.delete(self.resource_group, self.name) result = self.get_poller_result(poller) except AzureHttpError as exc: raise Exception("Error deleting security group {0} - {1}".format(self.name, str(exc))) return result def main(): AzureRMSecurityGroup() if __name__ == '__main__': main()