From b5d0b1d6ea69fc01befc1ca23794b1a93b3734d5 Mon Sep 17 00:00:00 2001 From: Evgeny Egorochkin Date: Sun, 24 Jan 2016 01:46:25 +0200 Subject: [PATCH] azure: network security groups: implement --- nix/azure-network-security-group.nix | 155 +++++++++++++ nix/azure.nix | 11 + nix/eval-machine-info.nix | 1 + nixops/azure_common.py | 1 + nixops/backends/azure_vm.py | 26 ++- .../resources/azure_network_security_group.py | 206 ++++++++++++++++++ 6 files changed, 399 insertions(+), 1 deletion(-) create mode 100644 nix/azure-network-security-group.nix create mode 100644 nixops/resources/azure_network_security_group.py diff --git a/nix/azure-network-security-group.nix b/nix/azure-network-security-group.nix new file mode 100644 index 000000000..8d6801f57 --- /dev/null +++ b/nix/azure-network-security-group.nix @@ -0,0 +1,155 @@ +{ config, lib, pkgs, uuid, name, resources, ... }: + +with lib; +with (import ./lib.nix lib); +let + + securityRuleOptions = { config, ... }: { + options = { + description = mkOption { + default = ""; + example = "Allow SSH"; + type = types.str; + description = "A description for this rule. Restricted to 140 characters."; + }; + + protocol = mkOption { + example = "Udp"; + type = types.enum [ "Tcp" "Udp" "*" ]; + description = "Network protocol this rule applies to. Can be Tcp, Udp or * to match both."; + }; + + sourcePortRange = mkOption { + example = "22"; + type = types.str; + description = "Source Port or Range. Integer or range between 0 and 65535 or * to match any."; + }; + + destinationPortRange = mkOption { + example = "22"; + type = types.str; + description = "Destination Port or Range. Integer or range between 0 and 65535 or * to match any."; + }; + + sourceAddressPrefix = mkOption { + example = "Internet"; + type = types.str; + description = '' + CIDR or source IP range or * to match any IP. + Tags such as "VirtualNetwork", "AzureLoadBalancer" and "Internet" + can also be used. + ''; + }; + + destinationAddressPrefix = mkOption { + example = "Internet"; + type = types.str; + description = '' + CIDR or destination IP range or * to match any IP. + Tags such as "VirtualNetwork", "AzureLoadBalancer" and "Internet" + can also be used. + ''; + }; + + access = mkOption { + example = "Allow"; + type = types.enum [ "Allow" "Deny" ]; + description = '' + Specifies whether network traffic is allowed or denied. + Possible values are "Allow" and "Deny". + ''; + }; + + priority = mkOption { + example = 2000; + type = types.int; + description = '' + Specifies the priority of the rule. + The value can be between 100 and 4096. + The priority number must be unique for + each rule in the collection. + The lower the priority number, + the higher the priority of the rule. + ''; + }; + + direction = mkOption { + example = "Inbound"; + type = types.enum [ "Inbound" "Outbound" ]; + description = '' + The direction specifies if rule will be evaluated + on incoming or outgoing traffic. + Possible values are "Inbound" and "Outbound". + ''; + }; + + }; + config = {}; + }; + + +in +{ + + options = (import ./azure-mgmt-credentials.nix lib "network security group") // { + + name = mkOption { + default = "nixops-${uuid}-${name}"; + example = "my-security-group"; + type = types.str; + description = "Name of the Azure network security group."; + }; + + resourceGroup = mkOption { + example = "xxx-my-group"; + type = types.either types.str (resource "azure-resource-group"); + description = '' + The name or resource of an Azure resource group + to create the network security group in. + ''; + }; + + location = mkOption { + example = "westus"; + type = types.str; + description = '' + The Azure data center location where the + network security group should be created. + ''; + }; + + tags = mkOption { + default = {}; + example = { environment = "production"; }; + type = types.attrsOf types.str; + description = "Tag name/value pairs to associate with the network security group."; + }; + + securityRules = mkOption { + default = {}; + example = { + allow-ssh = { + description = "Allow SSH"; + protocol = "Tcp"; + sourcePortRange = "*"; + destinationPortRange = "22"; + sourceAddressPrefix = "Internet"; + destinationAddressPrefix = "*"; + access = "Allow"; + priority = 2000; + direction = "Inbound"; + }; + }; + type = types.attrsOf types.optionSet; + options = securityRuleOptions; + description = "An attribute set of security rules."; + }; + + }; + + config = { + _type = "azure-network-security-group"; + resourceGroup = mkDefault resources.azureResourceGroups.def-group; + }; + +} diff --git a/nix/azure.nix b/nix/azure.nix index 4083dfb40..78081caf6 100644 --- a/nix/azure.nix +++ b/nix/azure.nix @@ -261,6 +261,17 @@ in Whether to obtain a dedicated public IP for the interface. ''; }; + + securityGroup = mkOption { + default = null; + example = "resources.azureSecurityGroups.my-security-group"; + type = types.nullOr (types.either types.str (resource "azure-network-security-group")); + description = '' + The Azure Resource Id or NixOps resource of + the Azure network security group to associate to the interface. + ''; + }; + }; resourceGroup = mkOption { diff --git a/nix/eval-machine-info.nix b/nix/eval-machine-info.nix index 9ab0c09cc..1b25e0811 100644 --- a/nix/eval-machine-info.nix +++ b/nix/eval-machine-info.nix @@ -107,6 +107,7 @@ rec { resources.azureDirectories = evalResources ./azure-directory.nix (zipAttrs resourcesByType.azureDirectories or []); resources.azureFiles = evalResources ./azure-file.nix (zipAttrs resourcesByType.azureFiles or []); resources.azureLoadBalancers = evalResources ./azure-load-balancer.nix (zipAttrs resourcesByType.azureLoadBalancers or []); + resources.azureSecurityGroups = evalResources ./azure-network-security-group.nix (zipAttrs resourcesByType.azureSecurityGroups or []); resources.azureQueues = evalResources ./azure-queue.nix (zipAttrs resourcesByType.azureQueues or []); resources.azureReservedIPAddresses = evalResources ./azure-reserved-ip-address.nix (zipAttrs resourcesByType.azureReservedIPAddresses or []); resources.azureResourceGroups = diff --git a/nixops/azure_common.py b/nixops/azure_common.py index 449b872d1..d7e61e879 100644 --- a/nixops/azure_common.py +++ b/nixops/azure_common.py @@ -88,6 +88,7 @@ def parse(cls, r_id): 'azure-load-balancer': { 'provider': 'Microsoft.Network', 'type': 'loadBalancers' }, 'azure-reserved-ip-address': {'provider': 'Microsoft.Network', 'type': 'publicIPAddresses' }, 'azure-virtual-network': {'provider':'Microsoft.Network', 'type': 'virtualNetworks' }, + 'azure-network-security-group': { 'provider':'Microsoft.Network', 'type': 'networkSecurityGroups' }, } diff --git a/nixops/backends/azure_vm.py b/nixops/backends/azure_vm.py index 85b6fda3a..b30db3420 100644 --- a/nixops/backends/azure_vm.py +++ b/nixops/backends/azure_vm.py @@ -31,6 +31,7 @@ from nixops.resources.azure_directory import AzureDirectoryState from nixops.resources.azure_file import AzureFileState from nixops.resources.azure_load_balancer import AzureLoadBalancerState +from nixops.resources.azure_network_security_group import AzureNetworkSecurityGroupState from nixops.resources.azure_queue import AzureQueueState from nixops.resources.azure_reserved_ip_address import AzureReservedIPAddressState from nixops.resources.azure_resource_group import AzureResourceGroupState @@ -105,6 +106,7 @@ def __init__(self, xml, config): ifaces_xml = x.find("attr[@name='networkInterfaces']") if_xml = ifaces_xml.find("attrs/attr[@name='default']") self.obtain_ip = self.get_option_value(if_xml, 'obtainIP', bool) + self.copy_option(if_xml, 'securityGroup', 'res-id', optional = True) subnet_xml = if_xml.find("attrs/attr[@name='subnet']") self.subnet = ResId(self.get_option_value(subnet_xml, 'network', 'res-id'), @@ -206,6 +208,7 @@ def get_type(cls): resource_group = attr_property("azure.resourceGroup", None) obtain_ip = attr_property("azure.obtainIP", None, bool) + security_group = attr_property("azure.securityGroup", None) availability_set = attr_property("azure.availabilitySet", None) block_device_mapping = attr_property("azure.blockDeviceMapping", {}, 'json') @@ -348,6 +351,8 @@ def check_network_iface(self): iface = None if iface: self.handle_changed_property('subnet', iface.ip_configurations[0].subnet.id) + self.handle_changed_property('security_group', iface.network_security_group and + iface.network_security_group.id) backend_address_pools = [ r.id for r in iface.ip_configurations[0].load_balancer_backend_address_pools ] self.handle_changed_property('backend_address_pools', sorted(backend_address_pools)) inbound_nat_rules = [ r.id for r in iface.ip_configurations[0].load_balancer_inbound_nat_rules ] @@ -662,22 +667,41 @@ def copy_iface_properties(self, defn): self.backend_address_pools = defn.backend_address_pools self.inbound_nat_rules = defn.inbound_nat_rules self.obtain_ip = defn.obtain_ip + self.security_group = defn.security_group self.subnet = defn.subnet def iface_properties_changed(self, defn): return ( self.backend_address_pools != defn.backend_address_pools or self.inbound_nat_rules != defn.inbound_nat_rules or self.obtain_ip != defn.obtain_ip or + self.security_group != defn.security_group or self.subnet != defn.subnet ) def create_or_update_iface(self, defn): public_ip_id = self.nrpc().public_ip_addresses.get( self.resource_group, self.public_ip).public_ip_address.id if defn.obtain_ip else None + print NetworkInterface(name = self.machine_name, + location = defn.location, + network_security_group = defn.security_group and + ResId(defn.security_group), + ip_configurations = [ NetworkInterfaceIpConfiguration( + name = 'default', + private_ip_allocation_method = IpAllocationMethod.dynamic, + subnet = ResId(defn.subnet), + load_balancer_backend_address_pools = [ + ResId(pool) for pool in defn.backend_address_pools ], + load_balancer_inbound_nat_rules = [ + ResId(rule) for rule in defn.inbound_nat_rules ], + public_ip_address = public_ip_id and ResId(public_ip_id), + )] + ).__dict__ self.nrpc().network_interfaces.create_or_update( self.resource_group, self.machine_name, NetworkInterface(name = self.machine_name, location = defn.location, + network_security_group = defn.security_group and + ResId(defn.security_group), ip_configurations = [ NetworkInterfaceIpConfiguration( name = 'default', private_ip_allocation_method = IpAllocationMethod.dynamic, @@ -1139,7 +1163,7 @@ def create_after(self, resources, defn): isinstance(r, AzureDirectoryState) or isinstance(r, AzureFileState) or isinstance(r, AzureLoadBalancerState) or isinstance(r, AzureQueueState) or isinstance(r, AzureReservedIPAddressState) or isinstance(r, AzureShareState) or - isinstance(r, AzureTableState)} + isinstance(r, AzureTableState) or isinstance(r, AzureNetworkSecurityGroupState) } def find_lb_endpoint(self): for _inr in self.inbound_nat_rules: diff --git a/nixops/resources/azure_network_security_group.py b/nixops/resources/azure_network_security_group.py new file mode 100644 index 000000000..71ecdbda1 --- /dev/null +++ b/nixops/resources/azure_network_security_group.py @@ -0,0 +1,206 @@ +# -*- coding: utf-8 -*- + +# Automatic provisioning of Azure network security groups. + +import os +import azure + +from nixops.util import attr_property +from nixops.azure_common import ResourceDefinition, ResourceState, ResId + +from azure.mgmt.network import * + +class AzureNetworkSecurityGroupDefinition(ResourceDefinition): + """Definition of an Azure Network Security Group""" + + @classmethod + def get_type(cls): + return "azure-network-security-group" + + @classmethod + def get_resource_type(cls): + return "azureSecurityGroups" + + def __init__(self, xml): + ResourceDefinition.__init__(self, xml) + + self.nsg_name = self.get_option_value(xml, 'name', str) + self.copy_option(xml, 'resourceGroup', 'resource') + self.copy_option(xml, 'location', str, empty = False) + self.copy_tags(xml) + + self.security_rules = { + _r.get("name"): self._parse_security_rule(_r) + for _r in xml.findall("attrs/attr[@name='securityRules']/attrs/attr") + } + + def _parse_security_rule(self, xml): + return { + 'description': self.get_option_value(xml, 'description', str), + 'protocol': self.get_option_value(xml, 'protocol', str), + 'source_port_range': self.get_option_value(xml, 'sourcePortRange', str), + 'destination_port_range': self.get_option_value(xml, 'destinationPortRange', str), + 'source_address_prefix': self.get_option_value(xml, 'sourceAddressPrefix', str), + 'destination_address_prefix': self.get_option_value(xml, 'destinationAddressPrefix', str), + 'access': self.get_option_value(xml, 'access', str), + 'priority': self.get_option_value(xml, 'priority', int), + 'direction': self.get_option_value(xml, 'direction', str), + } + + def show_type(self): + return "{0} [{1}]".format(self.get_type(), self.location) + + +class AzureNetworkSecurityGroupState(ResourceState): + """State of an Azure Network Security Group""" + + nsg_name = attr_property("azure.name", None) + resource_group = attr_property("azure.resourceGroup", None) + location = attr_property("azure.location", None) + tags = attr_property("azure.tags", {}, 'json') + security_rules = attr_property("azure.securityRules", {}, 'json') + + @classmethod + def get_type(cls): + return "azure-network-security-group" + + def show_type(self): + s = super(AzureNetworkSecurityGroupState, self).show_type() + if self.state == self.UP: s = "{0} [{1}]".format(s, self.location) + return s + + @property + def resource_id(self): + return self.nsg_name + + @property + def full_name(self): + return "Azure network security group '{0}'".format(self.resource_id) + + def get_resource(self): + try: + return self.nrpc().network_security_groups.get( + self.resource_group, self.resource_id).network_security_group + except azure.common.AzureMissingResourceHttpError: + return None + + def destroy_resource(self): + self.nrpc().network_security_groups.delete( + self.resource_group, self.resource_id) + + defn_properties = [ 'location', 'tags', 'security_rules' ] + + def _create_or_update(self, defn): + self.nrpc().network_security_groups.create_or_update( + defn.resource_group, defn.nsg_name, + NetworkSecurityGroup( + location = defn.location, + security_rules = [ + SecurityRule( + name = _name, + description = _r['description'], + protocol = _r['protocol'], + source_port_range = _r['source_port_range'], + destination_port_range = _r['destination_port_range'], + source_address_prefix = _r['source_address_prefix'], + destination_address_prefix = _r['destination_address_prefix'], + access = _r['access'], + priority = _r['priority'], + direction = _r['direction'], + ) for _name, _r in defn.security_rules.iteritems() + ], + tags = defn.tags)) + self.state = self.UP + self.copy_properties(defn) + + + def handle_changed_security_rules(self, rules): + def update_rules(k, v): + x = self.security_rules + if v == None: + x.pop(k, None) + else: + x[k] = v + self.security_rules = x + + for _rule in rules: + _s_name = next((_n for _n, _x in self.security_rules.iteritems() if _n == _rule.name), None) + if _s_name is None: + self.warn("found unexpected security rule {0}".format(_rule.name)) + update_rules(_rule.name, {"dummy": True}) + for _name, _s_rule in self.security_rules.iteritems(): + if _s_rule.get("dummy", False): continue + rule_res_name = "security rule {0}".format(_name) + rule = next((_r for _r in rules if _r.name == _name), None) + if rule is None: + self.warn("{0} has been deleted behind our back".format(rule_res_name)) + update_rules(_name, None) + continue + self.handle_changed_dict(_s_rule, 'description', + rule.description, + resource_name = rule_res_name) + self.handle_changed_dict(_s_rule, 'protocol', + rule.protocol, + resource_name = rule_res_name) + self.handle_changed_dict(_s_rule, 'source_port_range', + rule.source_port_range, + resource_name = rule_res_name) + self.handle_changed_dict(_s_rule, 'destination_port_range', + rule.destination_port_range, + resource_name = rule_res_name) + self.handle_changed_dict(_s_rule, 'source_address_prefix', + rule.source_address_prefix, + resource_name = rule_res_name) + self.handle_changed_dict(_s_rule, 'destination_address_prefix', + rule.destination_address_prefix, + resource_name = rule_res_name) + self.handle_changed_dict(_s_rule, 'access', + rule.access, + resource_name = rule_res_name) + self.handle_changed_dict(_s_rule, 'priority', + rule.priority, + resource_name = rule_res_name) + self.handle_changed_dict(_s_rule, 'direction', + rule.direction, + resource_name = rule_res_name) + update_rules(_name, _s_rule) + + + def create(self, defn, check, allow_reboot, allow_recreate): + self.no_property_change(defn, 'location') + self.no_property_change(defn, 'resource_group') + + self.copy_mgmt_credentials(defn) + self.nsg_name = defn.nsg_name + self.resource_group = defn.resource_group + + if check: + nsg = self.get_settled_resource() + if not nsg: + self.warn_missing_resource() + elif self.state == self.UP: + self.handle_changed_property('location', nsg.location, can_fix = False) + self.handle_changed_property('tags', nsg.tags) + self.handle_changed_security_rules(nsg.security_rules) + else: + self.warn_not_supposed_to_exist() + self.confirm_destroy() + + if self.state != self.UP: + if self.get_settled_resource(): + raise Exception("tried creating a network security group that already exists; " + "please run 'deploy --check' to fix this") + + self.log("creating {0} in {1}...".format(self.full_name, defn.location)) + self._create_or_update(defn) + + if self.properties_changed(defn): + self.log("updating properties of {0}...".format(self.full_name)) + self.get_settled_resource_assert_exists() + self._create_or_update(defn) + + + def create_after(self, resources, defn): + from nixops.resources.azure_resource_group import AzureResourceGroupState + return {r for r in resources + if isinstance(r, AzureResourceGroupState) }