From 43760d46704f366c580d62200cdf1e312a00f780 Mon Sep 17 00:00:00 2001 From: stack72 Date: Thu, 7 Jan 2016 12:23:08 +0000 Subject: [PATCH] Scaffolding for the AzureRM Network Security Groups --- builtin/providers/azurerm/provider.go | 1 + .../azurerm/resource_arm_security_group.go | 310 ++++++++++++++++++ .../resource_arm_security_group_test.go | 283 ++++++++++++++++ .../azurerm/r/security_group.html.markdown | 83 +++++ website/source/layouts/azurerm.erb | 4 + 5 files changed, 681 insertions(+) create mode 100644 builtin/providers/azurerm/resource_arm_security_group.go create mode 100644 builtin/providers/azurerm/resource_arm_security_group_test.go create mode 100644 website/source/docs/providers/azurerm/r/security_group.html.markdown diff --git a/builtin/providers/azurerm/provider.go b/builtin/providers/azurerm/provider.go index 612109fc3..1642c64d3 100644 --- a/builtin/providers/azurerm/provider.go +++ b/builtin/providers/azurerm/provider.go @@ -43,6 +43,7 @@ func Provider() terraform.ResourceProvider { "azurerm_virtual_network": resourceArmVirtualNetwork(), "azurerm_local_network_gateway": resourceArmLocalNetworkGateway(), "azurerm_availability_set": resourceArmAvailabilitySet(), + "azurerm_security_group": resourceArmSecurityGroup(), }, ConfigureFunc: providerConfigure, diff --git a/builtin/providers/azurerm/resource_arm_security_group.go b/builtin/providers/azurerm/resource_arm_security_group.go new file mode 100644 index 000000000..e523ea2f6 --- /dev/null +++ b/builtin/providers/azurerm/resource_arm_security_group.go @@ -0,0 +1,310 @@ +package azurerm + +import ( + "bytes" + "fmt" + "log" + "net/http" + "time" + + "strings" + + "github.com/Azure/azure-sdk-for-go/arm/network" + "github.com/hashicorp/terraform/helper/hashcode" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/helper/schema" +) + +func resourceArmSecurityGroup() *schema.Resource { + return &schema.Resource{ + Create: resourceArmSecurityGroupCreate, + Read: resourceArmSecurityGroupRead, + Update: resourceArmSecurityGroupCreate, + Delete: resourceArmSecurityGroupDelete, + + Schema: map[string]*schema.Schema{ + "name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "location": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + StateFunc: azureRMNormalizeLocation, + }, + + "resource_group_name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "security_rule": &schema.Schema{ + Type: schema.TypeSet, + Optional: true, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + + "description": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ValidateFunc: func(v interface{}, k string) (ws []string, errors []error) { + value := v.(string) + if len(value) > 140 { + errors = append(errors, fmt.Errorf( + "The security rule description can be no longer than 140 chars")) + } + return + }, + }, + + "protocol": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ValidateFunc: validateSecurityRuleProtocol, + }, + + "source_port_range": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + + "destination_port_range": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + + "source_address_prefix": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + + "destination_address_prefix": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + + "access": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ValidateFunc: validateSecurityRuleAccess, + }, + + "priority": &schema.Schema{ + Type: schema.TypeInt, + Required: true, + ValidateFunc: func(v interface{}, k string) (ws []string, errors []error) { + value := v.(int) + if value < 100 || value > 4096 { + errors = append(errors, fmt.Errorf( + "The `priority` can only be between 100 and 4096")) + } + return + }, + }, + + "direction": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ValidateFunc: validateSecurityRuleDirection, + }, + }, + }, + Set: resourceArmSecurityGroupRuleHash, + }, + }, + } +} + +func resourceArmSecurityGroupCreate(d *schema.ResourceData, meta interface{}) error { + client := meta.(*ArmClient) + secClient := client.secGroupClient + + name := d.Get("name").(string) + location := d.Get("location").(string) + resGroup := d.Get("resource_group_name").(string) + + sgRules, sgErr := expandAzureRmSecurityGroupRules(d) + if sgErr != nil { + return fmt.Errorf("Error Building list of Security Group Rules: %s", sgErr) + } + + sg := network.SecurityGroup{ + Name: &name, + Location: &location, + Properties: &network.SecurityGroupPropertiesFormat{ + SecurityRules: &sgRules, + }, + } + + resp, err := secClient.CreateOrUpdate(resGroup, name, sg) + if err != nil { + return err + } + + d.SetId(*resp.ID) + + log.Printf("[DEBUG] Waiting for Security Group (%s) to become available", name) + stateConf := &resource.StateChangeConf{ + Pending: []string{"Accepted", "Updating"}, + Target: "Succeeded", + Refresh: securityGroupStateRefreshFunc(client, resGroup, name), + Timeout: 10 * time.Minute, + } + if _, err := stateConf.WaitForState(); err != nil { + return fmt.Errorf("Error waiting for Securty Group (%s) to become available: %s", name, err) + } + + return resourceArmSecurityGroupRead(d, meta) +} + +func resourceArmSecurityGroupRead(d *schema.ResourceData, meta interface{}) error { + secGroupClient := meta.(*ArmClient).secGroupClient + + id, err := parseAzureResourceID(d.Id()) + if err != nil { + return err + } + resGroup := id.ResourceGroup + name := id.Path["networkSecurityGroups"] + + resp, err := secGroupClient.Get(resGroup, name) + if resp.StatusCode == http.StatusNotFound { + d.SetId("") + return nil + } + if err != nil { + return fmt.Errorf("Error making Read request on Azure Security Group %s: %s", name, err) + } + + return nil +} + +func resourceArmSecurityGroupDelete(d *schema.ResourceData, meta interface{}) error { + secGroupClient := meta.(*ArmClient).secGroupClient + + id, err := parseAzureResourceID(d.Id()) + if err != nil { + return err + } + resGroup := id.ResourceGroup + name := id.Path["networkSecurityGroups"] + + _, err = secGroupClient.Delete(resGroup, name) + + return err +} + +func resourceArmSecurityGroupRuleHash(v interface{}) int { + var buf bytes.Buffer + m := v.(map[string]interface{}) + buf.WriteString(fmt.Sprintf("%s-", m["protocol"].(string))) + buf.WriteString(fmt.Sprintf("%s-", m["source_port_range"].(string))) + buf.WriteString(fmt.Sprintf("%s-", m["destination_port_range"].(string))) + buf.WriteString(fmt.Sprintf("%s-", m["source_address_prefix"].(string))) + buf.WriteString(fmt.Sprintf("%s-", m["destination_address_prefix"].(string))) + buf.WriteString(fmt.Sprintf("%s-", m["access"].(string))) + buf.WriteString(fmt.Sprintf("%d-", m["priority"].(int))) + buf.WriteString(fmt.Sprintf("%s-", m["direction"].(string))) + + return hashcode.String(buf.String()) +} + +func securityGroupStateRefreshFunc(client *ArmClient, resourceGroupName string, securityGroupName string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + res, err := client.secGroupClient.Get(resourceGroupName, securityGroupName) + if err != nil { + return nil, "", fmt.Errorf("Error issuing read request in securityGroupStateRefreshFunc to Azure ARM for security group '%s' (RG: '%s'): %s", securityGroupName, resourceGroupName, err) + } + + return res, *res.Properties.ProvisioningState, nil + } +} + +func expandAzureRmSecurityGroupRules(d *schema.ResourceData) ([]network.SecurityRule, error) { + sgRules := d.Get("security_rule").(*schema.Set).List() + rules := make([]network.SecurityRule, 0, len(sgRules)) + + for _, sgRaw := range sgRules { + data := sgRaw.(map[string]interface{}) + + source_port_range := data["source_port_range"].(string) + destination_port_range := data["destination_port_range"].(string) + source_address_prefix := data["source_address_prefix"].(string) + destination_address_prefix := data["destination_address_prefix"].(string) + priority := data["priority"].(int) + + properties := network.SecurityRulePropertiesFormat{ + SourcePortRange: &source_port_range, + DestinationPortRange: &destination_port_range, + SourceAddressPrefix: &source_address_prefix, + DestinationAddressPrefix: &destination_address_prefix, + Priority: &priority, + Access: network.SecurityRuleAccess(data["access"].(string)), + Direction: network.SecurityRuleDirection(data["direction"].(string)), + Protocol: network.SecurityRuleProtocol(data["protocol"].(string)), + } + + if v := data["description"].(string); v != "" { + properties.Description = &v + } + + name := data["name"].(string) + rule := network.SecurityRule{ + Name: &name, + Properties: &properties, + } + + rules = append(rules, rule) + } + + return rules, nil +} + +func validateSecurityRuleProtocol(v interface{}, k string) (ws []string, errors []error) { + value := strings.ToLower(v.(string)) + viewTypes := map[string]bool{ + "tcp": true, + "udp": true, + "*": true, + } + + if !viewTypes[value] { + errors = append(errors, fmt.Errorf("Security Rule Protocol can only be Tcp, Udp or *")) + } + return +} + +func validateSecurityRuleAccess(v interface{}, k string) (ws []string, errors []error) { + value := strings.ToLower(v.(string)) + viewTypes := map[string]bool{ + "allow": true, + "deny": true, + } + + if !viewTypes[value] { + errors = append(errors, fmt.Errorf("Security Rule Access can only be Allow or Deny")) + } + return +} + +func validateSecurityRuleDirection(v interface{}, k string) (ws []string, errors []error) { + value := strings.ToLower(v.(string)) + viewTypes := map[string]bool{ + "inbound": true, + "outbound": true, + } + + if !viewTypes[value] { + errors = append(errors, fmt.Errorf("Security Rule Directions can only be Inbound or Outbound")) + } + return +} diff --git a/builtin/providers/azurerm/resource_arm_security_group_test.go b/builtin/providers/azurerm/resource_arm_security_group_test.go new file mode 100644 index 000000000..8431399e0 --- /dev/null +++ b/builtin/providers/azurerm/resource_arm_security_group_test.go @@ -0,0 +1,283 @@ +package azurerm + +import ( + "fmt" + "net/http" + "testing" + + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestResourceAzureRMSecurityGroupProtocol_validation(t *testing.T) { + cases := []struct { + Value string + ErrCount int + }{ + { + Value: "Random", + ErrCount: 1, + }, + { + Value: "tcp", + ErrCount: 0, + }, + { + Value: "TCP", + ErrCount: 0, + }, + { + Value: "*", + ErrCount: 0, + }, + { + Value: "Udp", + ErrCount: 0, + }, + { + Value: "Tcp", + ErrCount: 0, + }, + } + + for _, tc := range cases { + _, errors := validateSecurityRuleProtocol(tc.Value, "azurerm_security_group") + + if len(errors) != tc.ErrCount { + t.Fatalf("Expected the Azure RM Security Group protocol to trigger a validation error") + } + } +} + +func TestResourceAzureRMSecurityGroupAccess_validation(t *testing.T) { + cases := []struct { + Value string + ErrCount int + }{ + { + Value: "Random", + ErrCount: 1, + }, + { + Value: "Allow", + ErrCount: 0, + }, + { + Value: "Deny", + ErrCount: 0, + }, + { + Value: "ALLOW", + ErrCount: 0, + }, + { + Value: "deny", + ErrCount: 0, + }, + } + + for _, tc := range cases { + _, errors := validateSecurityRuleAccess(tc.Value, "azurerm_security_group") + + if len(errors) != tc.ErrCount { + t.Fatalf("Expected the Azure RM Security Group access to trigger a validation error") + } + } +} + +func TestResourceAzureRMSecurityGroupDirection_validation(t *testing.T) { + cases := []struct { + Value string + ErrCount int + }{ + { + Value: "Random", + ErrCount: 1, + }, + { + Value: "Inbound", + ErrCount: 0, + }, + { + Value: "Outbound", + ErrCount: 0, + }, + { + Value: "INBOUND", + ErrCount: 0, + }, + { + Value: "Inbound", + ErrCount: 0, + }, + } + + for _, tc := range cases { + _, errors := validateSecurityRuleDirection(tc.Value, "azurerm_security_group") + + if len(errors) != tc.ErrCount { + t.Fatalf("Expected the Azure RM Security Group direction to trigger a validation error") + } + } +} + +func TestAccAzureRMSecurityGroup_basic(t *testing.T) { + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testCheckAzureRMSecurityGroupDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccAzureRMSecurityGroup_basic, + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMSecurityGroupExists("azurerm_security_group.test"), + ), + }, + }, + }) +} + +func TestAccAzureRMSecurityGroup_addingExtraRules(t *testing.T) { + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testCheckAzureRMSecurityGroupDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccAzureRMSecurityGroup_basic, + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMSecurityGroupExists("azurerm_security_group.test"), + resource.TestCheckResourceAttr( + "azurerm_security_group.test", "security_rule.#", "1"), + ), + }, + + resource.TestStep{ + Config: testAccAzureRMSecurityGroup_anotherRule, + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMSecurityGroupExists("azurerm_security_group.test"), + resource.TestCheckResourceAttr( + "azurerm_security_group.test", "security_rule.#", "2"), + ), + }, + }, + }) +} + +func testCheckAzureRMSecurityGroupExists(name string) resource.TestCheckFunc { + return func(s *terraform.State) error { + + rs, ok := s.RootModule().Resources[name] + if !ok { + return fmt.Errorf("Not found: %s", name) + } + + sgName := rs.Primary.Attributes["name"] + resourceGroup, hasResourceGroup := rs.Primary.Attributes["resource_group_name"] + if !hasResourceGroup { + return fmt.Errorf("Bad: no resource group found in state for security group: %s", sgName) + } + + conn := testAccProvider.Meta().(*ArmClient).secGroupClient + + resp, err := conn.Get(resourceGroup, sgName) + if err != nil { + return fmt.Errorf("Bad: Get on secGroupClient: %s", err) + } + + if resp.StatusCode == http.StatusNotFound { + return fmt.Errorf("Bad: Security Group %q (resource group: %q) does not exist", name, resourceGroup) + } + + return nil + } +} + +func testCheckAzureRMSecurityGroupDestroy(s *terraform.State) error { + conn := testAccProvider.Meta().(*ArmClient).secGroupClient + + for _, rs := range s.RootModule().Resources { + if rs.Type != "azurerm_security_group" { + continue + } + + name := rs.Primary.Attributes["name"] + resourceGroup := rs.Primary.Attributes["resource_group_name"] + + resp, err := conn.Get(resourceGroup, name) + + if err != nil { + return nil + } + + if resp.StatusCode != http.StatusNotFound { + return fmt.Errorf("Security Group still exists:\n%#v", resp.Properties) + } + } + + return nil +} + +var testAccAzureRMSecurityGroup_basic = ` +resource "azurerm_resource_group" "test" { + name = "acceptanceTestResourceGroup1" + location = "West US" +} + +resource "azurerm_security_group" "test" { + name = "acceptanceTestSecurityGroup1" + location = "West US" + resource_group_name = "${azurerm_resource_group.test.name}" + + security_rule { + name = "test123" + priority = 100 + direction = "Inbound" + access = "Allow" + protocol = "Tcp" + source_port_range = "*" + destination_port_range = "*" + source_address_prefix = "*" + destination_address_prefix = "*" + } +} +` + +var testAccAzureRMSecurityGroup_anotherRule = ` +resource "azurerm_resource_group" "test" { + name = "acceptanceTestResourceGroup1" + location = "West US" +} + +resource "azurerm_security_group" "test" { + name = "acceptanceTestSecurityGroup1" + location = "West US" + resource_group_name = "${azurerm_resource_group.test.name}" + + security_rule { + name = "test123" + priority = 100 + direction = "Inbound" + access = "Allow" + protocol = "Tcp" + source_port_range = "*" + destination_port_range = "*" + source_address_prefix = "*" + destination_address_prefix = "*" + } + + security_rule { + name = "testDeny" + priority = 101 + direction = "Inbound" + access = "Deny" + protocol = "Udp" + source_port_range = "*" + destination_port_range = "*" + source_address_prefix = "*" + destination_address_prefix = "*" + } +} +` diff --git a/website/source/docs/providers/azurerm/r/security_group.html.markdown b/website/source/docs/providers/azurerm/r/security_group.html.markdown new file mode 100644 index 000000000..f138f4c12 --- /dev/null +++ b/website/source/docs/providers/azurerm/r/security_group.html.markdown @@ -0,0 +1,83 @@ +--- +layout: "azurerm" +page_title: "Azure Resource Manager: azurerm_security_group" +sidebar_current: "docs-azurerm-resource-security-group" +description: |- + Create a network security group that contains a list of network security rules. Network security groups enable inbound or outbound traffic to be enabled or denied. +--- + +# azurerm\_security\_group + +Create a network security group that contains a list of network security rules. + +## Example Usage + +``` +resource "azurerm_resource_group" "test" { + name = "acceptanceTestResourceGroup1" + location = "West US" +} + +resource "azurerm_security_group" "test" { + name = "acceptanceTestSecurityGroup1" + location = "West US" + resource_group_name = "${azurerm_resource_group.test.name}" + + security_rule { + name = "test123" + priority = 100 + direction = "Inbound" + access = "Allow" + protocol = "Tcp" + source_port_range = "*" + destination_port_range = "*" + source_address_prefix = "*" + destination_address_prefix = "*" + } +} +``` + +## Argument Reference + +The following arguments are supported: + +* `name` - (Required) Specifies the name of the availability set. Changing this forces a + new resource to be created. + +* `resource_group_name` - (Required) The name of the resource group in which to + create the availability set. + +* `location` - (Required) Specifies the supported Azure location where the resource exists. Changing this forces a new resource to be created. + +* `security_rule` - (Optional) Can be specified multiple times to define multiple + security rules. Each `security_rule` block supports fields documented below. + + +The `security_rule` block supports: + +* `name` - (Required) The name of the security rule. + +* `description` - (Optional) A description for this rule. Restricted to 140 characters. + +* `protocol` - (Required) Network protocol this rule applies to. Can be Tcp, Udp or * to match both. + +* `source_port_range` - (Required) Source Port or Range. Integer or range between 0 and 65535 or * to match any. + +* `destination_port_range` - (Required) Destination Port or Range. Integer or range between 0 and 65535 or * to match any. + +* `source_address_prefix` - (Required) CIDR or source IP range or * to match any IP. Tags such as ‘VirtualNetwork’, ‘AzureLoadBalancer’ and ‘Internet’ can also be used. + +* `destination_address_prefix` - (Required) CIDR or destination IP range or * to match any IP. Tags such as ‘VirtualNetwork’, ‘AzureLoadBalancer’ and ‘Internet’ can also be used. + +* `access` - (Required) Specifies whether network traffic is allowed or denied. Possible values are “Allow” and “Deny”. + +* `priority` - (Required) 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` - (Required) The direction specifies if rule will be evaluated on incoming or outgoing traffic. Possible values are “Inbound” and “Outbound”. + + +## Attributes Reference + +The following attributes are exported: + +* `id` - The virtual AvailabilitySet ID. \ No newline at end of file diff --git a/website/source/layouts/azurerm.erb b/website/source/layouts/azurerm.erb index cbac83abf..77a0987a4 100644 --- a/website/source/layouts/azurerm.erb +++ b/website/source/layouts/azurerm.erb @@ -29,6 +29,10 @@ azurerm_availability_set + > + azurerm_security_group + +