diff --git a/builtin/providers/azure/azure.test b/builtin/providers/azure/azure.test new file mode 100755 index 0000000000..ec68e39e64 Binary files /dev/null and b/builtin/providers/azure/azure.test differ diff --git a/builtin/providers/azure/config.go b/builtin/providers/azure/config.go index c21f6f7057..5ff46084ec 100644 --- a/builtin/providers/azure/config.go +++ b/builtin/providers/azure/config.go @@ -5,7 +5,7 @@ import ( "os" "sync" - "github.com/svanharmelen/azure-sdk-for-go/management" + "github.com/Azure/azure-sdk-for-go/management" ) // Config is the configuration structure used to instantiate a diff --git a/builtin/providers/azure/constants.go b/builtin/providers/azure/constants.go new file mode 100644 index 0000000000..55264f9696 --- /dev/null +++ b/builtin/providers/azure/constants.go @@ -0,0 +1,48 @@ +package azure + +const ( + // terraformAzureLabel is used as the label for the hosted service created + // by Terraform on Azure. + terraformAzureLabel = "terraform-on-azure" + + // terraformAzureDescription is the description used for the hosted service + // created by Terraform on Azure. + terraformAzureDescription = "Hosted service automatically created by terraform." +) + +// parameterDescriptions holds a list of descriptions for all the available +// parameters of an Azure configuration. +var parameterDescriptions = map[string]string{ + // provider descriptions: + "management_url": "The URL of the management API all requests should be sent to.\n" + + "Defaults to 'https://management.core.windows.net/', which is the default Azure API URL.\n" + + "This should be filled in only if you have your own datacenter with its own hosted management API.", + "management_certificate": "The certificate for connecting to the management API specified with 'management_url'", + "subscription_id": "The subscription ID to be used when connecting to the management API.", + "publish_settings_file": "The publish settings file, either created by you or downloaded from 'https://manage.windowsazure.com/publishsettings'", + // general resource descriptions: + "name": "Name of the resource to be created as it will appear in the Azure dashboard.", + "service_name": "Name of the hosted service within Azure. Will have a DNS entry as dns-name.cloudapp.net", + "location": "The Azure location where the resource will be located.\n" + + "A list of Azure locations can be found here: http://azure.microsoft.com/en-us/regions/", + "reverse_dns_fqdn": "The reverse of the fully qualified domain name. Optional.", + "label": "Label by which the resource will be identified by. Optional.", + "description": "Brief description of the resource. Optional.", + // hosted service descriptions: + "ephemeral_contents": "Sets whether the associated contents of this resource should also be\n" + + "deleted upon this resource's deletion. Default is false.", + // instance descriptions: + "image": "The image the new VM will be booted from. Mandatory.", + "size": "The size in GB of the disk to be created. Mandatory.", + "os_type": "The OS type of the VM. Either Windows or Linux. Mandatory.", + "storage_account": "The storage account (pool) name. Mandatory.", + "storage_container": "The storage container name from the storage pool given with 'storage_pool'.", + "user_name": "The user name to be configured on the new VM.", + "user_password": "The user password to be configured on the new VM.", + "default_certificate_thumbprint": "The thumbprint of the WinRM Certificate to be used as a default.", + // local network descriptions: + "vpn_gateway_address": "The IP address of the VPN gateway bridged through this virtual network.", + "address_space_prefixes": "List of address space prefixes in the format '/netmask'", + // dns descriptions: + "dns_address": "Address of the DNS server. Required.", +} diff --git a/builtin/providers/azure/provider.go b/builtin/providers/azure/provider.go index eef0ca49c3..dbb6eeb869 100644 --- a/builtin/providers/azure/provider.go +++ b/builtin/providers/azure/provider.go @@ -32,10 +32,17 @@ func Provider() terraform.ResourceProvider { }, ResourcesMap: map[string]*schema.Resource{ - "azure_data_disk": resourceAzureDataDisk(), - "azure_instance": resourceAzureInstance(), - "azure_security_group": resourceAzureSecurityGroup(), - "azure_virtual_network": resourceAzureVirtualNetwork(), + "azure_instance": resourceAzureInstance(), + "azure_data_disk": resourceAzureDataDisk(), + "azure_hosted_service": resourceAzureHostedService(), + "azure_storage_service": resourceAzureStorageService(), + "azure_storage_container": resourceAzureStorageContainer(), + "azure_storage_blob": resourceAzureStorageBlob(), + "azure_virtual_network": resourceAzureVirtualNetwork(), + "azure_dns_server": resourceAzureDnsServer(), + "azure_local_network_connection": resourceAzureLocalNetworkConnection(), + "azure_security_group": resourceAzureSecurityGroup(), + "azure_security_group_rule": resourceAzureSecurityGroupRule(), }, ConfigureFunc: providerConfigure, diff --git a/builtin/providers/azure/provider_test.go b/builtin/providers/azure/provider_test.go index 5403df3ef8..51f78e3949 100644 --- a/builtin/providers/azure/provider_test.go +++ b/builtin/providers/azure/provider_test.go @@ -11,6 +11,17 @@ import ( var testAccProviders map[string]terraform.ResourceProvider var testAccProvider *schema.Provider +const testAccSecurityGroupName = "terraform-security-group" + +// testAccStorageServiceName is used as the name for the Storage Service +// created in all storage-related tests. +// It is much more convenient to provide a Storage Service which +// has been created beforehand as the creation of one takes a lot +// and would greatly impede the multitude of tests which rely on one. +var testAccStorageServiceName = os.Getenv("AZURE_STORAGE") + +const testAccStorageContainerName = "terraform-testing-container" + func init() { testAccProvider = Provider().(*schema.Provider) testAccProviders = map[string]terraform.ResourceProvider{ diff --git a/builtin/providers/azure/resource_azure_data_disk.go b/builtin/providers/azure/resource_azure_data_disk.go index 39316a49c7..eb8a6af00d 100644 --- a/builtin/providers/azure/resource_azure_data_disk.go +++ b/builtin/providers/azure/resource_azure_data_disk.go @@ -5,9 +5,9 @@ import ( "log" "time" + "github.com/Azure/azure-sdk-for-go/management" + "github.com/Azure/azure-sdk-for-go/management/virtualmachinedisk" "github.com/hashicorp/terraform/helper/schema" - "github.com/svanharmelen/azure-sdk-for-go/management" - "github.com/svanharmelen/azure-sdk-for-go/management/virtualmachinedisk" ) const dataDiskBlobStorageURL = "http://%s.blob.core.windows.net/disks/%s.vhd" @@ -50,7 +50,7 @@ func resourceAzureDataDisk() *schema.Resource { Default: "None", }, - "storage": &schema.Schema{ + "storage_service_name": &schema.Schema{ Type: schema.TypeString, Optional: true, ForceNew: true, @@ -321,7 +321,7 @@ func mediaLink(d *schema.ResourceData) string { name = fmt.Sprintf("%s-%d", d.Get("virtual_machine").(string), d.Get("lun").(int)) } - return fmt.Sprintf(dataDiskBlobStorageURL, d.Get("storage").(string), name.(string)) + return fmt.Sprintf(dataDiskBlobStorageURL, d.Get("storage_service_name").(string), name.(string)) } func verifyDataDiskParameters(d *schema.ResourceData) error { @@ -332,7 +332,7 @@ func verifyDataDiskParameters(d *schema.ResourceData) error { } if _, ok := d.GetOk("media_link"); !ok { - if _, ok := d.GetOk("storage"); !ok { + if _, ok := d.GetOk("storage_service_name"); !ok { return fmt.Errorf("If not supplying 'media_link', you must supply 'storage'.") } } diff --git a/builtin/providers/azure/resource_azure_data_disk_test.go b/builtin/providers/azure/resource_azure_data_disk_test.go index 6eb3036104..3c7530f387 100644 --- a/builtin/providers/azure/resource_azure_data_disk_test.go +++ b/builtin/providers/azure/resource_azure_data_disk_test.go @@ -2,14 +2,13 @@ package azure import ( "fmt" - "os" "strconv" "testing" + "github.com/Azure/azure-sdk-for-go/management" + "github.com/Azure/azure-sdk-for-go/management/virtualmachinedisk" "github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/terraform" - "github.com/svanharmelen/azure-sdk-for-go/management" - "github.com/svanharmelen/azure-sdk-for-go/management/virtualmachinedisk" ) func TestAccAzureDataDisk_basic(t *testing.T) { @@ -174,7 +173,7 @@ resource "azure_instance" "foo" { name = "terraform-test" image = "Ubuntu Server 14.04 LTS" size = "Basic_A1" - storage = "%s" + storage_service_name = "%s" location = "West US" username = "terraform" password = "Pass!admin123" @@ -183,16 +182,16 @@ resource "azure_instance" "foo" { resource "azure_data_disk" "foo" { lun = 0 size = 10 - storage = "${azure_instance.foo.storage}" + storage_service_name = "${azure_instance.foo.storage}" virtual_machine = "${azure_instance.foo.id}" -}`, os.Getenv("AZURE_STORAGE")) +}`, testAccStorageServiceName) var testAccAzureDataDisk_advanced = fmt.Sprintf(` resource "azure_instance" "foo" { name = "terraform-test1" image = "Ubuntu Server 14.04 LTS" size = "Basic_A1" - storage = "%s" + storage_service_name = "%s" location = "West US" username = "terraform" password = "Pass!admin123" @@ -202,16 +201,16 @@ resource "azure_data_disk" "foo" { lun = 1 size = 10 caching = "ReadOnly" - storage = "${azure_instance.foo.storage}" + storage_service_name = "${azure_instance.foo.storage}" virtual_machine = "${azure_instance.foo.id}" -}`, os.Getenv("AZURE_STORAGE")) +}`, testAccStorageServiceName) var testAccAzureDataDisk_update = fmt.Sprintf(` resource "azure_instance" "foo" { name = "terraform-test1" image = "Ubuntu Server 14.04 LTS" size = "Basic_A1" - storage = "%s" + storage_service_name = "%s" location = "West US" username = "terraform" password = "Pass!admin123" @@ -221,7 +220,7 @@ resource "azure_instance" "bar" { name = "terraform-test2" image = "Ubuntu Server 14.04 LTS" size = "Basic_A1" - storage = "${azure_instance.foo.storage}" + storage_service_name = "${azure_instance.foo.storage}" location = "West US" username = "terraform" password = "Pass!admin123" @@ -231,6 +230,6 @@ resource "azure_data_disk" "foo" { lun = 2 size = 20 caching = "ReadWrite" - storage = "${azure_instance.bar.storage}" + storage_service_name = "${azure_instance.bar.storage}" virtual_machine = "${azure_instance.bar.id}" -}`, os.Getenv("AZURE_STORAGE")) +}`, testAccStorageServiceName) diff --git a/builtin/providers/azure/resource_azure_dns_server.go b/builtin/providers/azure/resource_azure_dns_server.go new file mode 100644 index 0000000000..30da0843a4 --- /dev/null +++ b/builtin/providers/azure/resource_azure_dns_server.go @@ -0,0 +1,246 @@ +package azure + +import ( + "fmt" + "log" + + "github.com/Azure/azure-sdk-for-go/management/virtualnetwork" + "github.com/hashicorp/terraform/helper/schema" +) + +// resourceAzureDnsServer returns the *schema.Resource associated +// to an Azure hosted service. +func resourceAzureDnsServer() *schema.Resource { + return &schema.Resource{ + Create: resourceAzureDnsServerCreate, + Read: resourceAzureDnsServerRead, + Update: resourceAzureDnsServerUpdate, + Exists: resourceAzureDnsServerExists, + Delete: resourceAzureDnsServerDelete, + + Schema: map[string]*schema.Schema{ + "name": &schema.Schema{ + Type: schema.TypeString, + ForceNew: true, + Required: true, + Description: parameterDescriptions["name"], + }, + "dns_address": &schema.Schema{ + Type: schema.TypeString, + Required: true, + Description: parameterDescriptions["dns_address"], + }, + }, + } +} + +// resourceAzureDnsServerCreate does all the necessary API calls +// to create a new DNS server definition on Azure. +func resourceAzureDnsServerCreate(d *schema.ResourceData, meta interface{}) error { + // first; check for the existence of the resource: + exists, err := resourceAzureDnsServerExists(d, meta) + if err != nil { + return err + } + if exists { + return fmt.Errorf("Azure DNS server definition already exists.") + } + + azureClient := meta.(*Client) + mgmtClient := azureClient.mgmtClient + networkClient := virtualnetwork.NewClient(mgmtClient) + + log.Println("[INFO] Fetching current network configuration from Azure.") + azureClient.mutex.Lock() + defer azureClient.mutex.Unlock() + netConf, err := networkClient.GetVirtualNetworkConfiguration() + if err != nil { + return fmt.Errorf("Failed to get the current network configuration from Azure: %s", err) + } + + log.Println("[DEBUG] Adding new DNS server definition to Azure.") + name := d.Get("name").(string) + address := d.Get("dns_address").(string) + netConf.Configuration.DNS.DNSServers = append( + netConf.Configuration.DNS.DNSServers, + virtualnetwork.DNSServer{ + Name: name, + IPAddress: address, + }) + + // send the configuration back to Azure: + log.Println("[INFO] Sending updated network configuration back to Azure.") + reqID, err := networkClient.SetVirtualNetworkConfiguration(netConf) + if err != nil { + return fmt.Errorf("Failed issuing update to network configuration: %s", err) + } + err = mgmtClient.WaitForOperation(reqID, nil) + if err != nil { + return fmt.Errorf("Error setting network configuration: %s", err) + } + + d.SetId(name) + return nil +} + +// resourceAzureDnsServerRead does all the necessary API calls to read +// the state of the DNS server off Azure. +func resourceAzureDnsServerRead(d *schema.ResourceData, meta interface{}) error { + azureClient := meta.(*Client) + mgmtClient := azureClient.mgmtClient + networkClient := virtualnetwork.NewClient(mgmtClient) + + log.Println("[INFO] Fetching current network configuration from Azure.") + netConf, err := networkClient.GetVirtualNetworkConfiguration() + if err != nil { + return fmt.Errorf("Failed to get the current network configuration from Azure: %s", err) + } + + var found bool + name := d.Get("name").(string) + + // search for our DNS and update it if the IP has been changed: + for _, dns := range netConf.Configuration.DNS.DNSServers { + if dns.Name == name { + found = true + d.Set("dns_address", dns.IPAddress) + break + } + } + + // remove the resource from the state if it has been deleted in the meantime: + if !found { + d.SetId("") + } + + return nil +} + +// resourceAzureDnsServerUpdate does all the necessary API calls +// to update the DNS definition on Azure. +func resourceAzureDnsServerUpdate(d *schema.ResourceData, meta interface{}) error { + azureClient := meta.(*Client) + mgmtClient := azureClient.mgmtClient + networkClient := virtualnetwork.NewClient(mgmtClient) + + var found bool + name := d.Get("name").(string) + + if d.HasChange("dns_address") { + log.Println("[DEBUG] DNS server address has changes; updating it on Azure.") + log.Println("[INFO] Fetching current network configuration from Azure.") + azureClient.mutex.Lock() + defer azureClient.mutex.Unlock() + netConf, err := networkClient.GetVirtualNetworkConfiguration() + if err != nil { + return fmt.Errorf("Failed to get the current network configuration from Azure: %s", err) + } + + // search for our DNS and update its address value: + for i, dns := range netConf.Configuration.DNS.DNSServers { + if dns.Name == name { + found = true + netConf.Configuration.DNS.DNSServers[i].IPAddress = d.Get("dns_address").(string) + break + } + } + + // if the config has changes, send the configuration back to Azure: + if found { + log.Println("[INFO] Sending updated network configuration back to Azure.") + reqID, err := networkClient.SetVirtualNetworkConfiguration(netConf) + if err != nil { + return fmt.Errorf("Failed issuing update to network configuration: %s", err) + } + err = mgmtClient.WaitForOperation(reqID, nil) + if err != nil { + return fmt.Errorf("Error setting network configuration: %s", err) + } + + return nil + } + } + + // remove the resource from the state if it has been deleted in the meantime: + if !found { + d.SetId("") + } + + return nil +} + +// resourceAzureDnsServerExists does all the necessary API calls to +// check if the DNS server definition alredy exists on Azure. +func resourceAzureDnsServerExists(d *schema.ResourceData, meta interface{}) (bool, error) { + azureClient := meta.(*Client) + mgmtClient := azureClient.mgmtClient + networkClient := virtualnetwork.NewClient(mgmtClient) + + log.Println("[INFO] Fetching current network configuration from Azure.") + netConf, err := networkClient.GetVirtualNetworkConfiguration() + if err != nil { + return false, fmt.Errorf("Failed to get the current network configuration from Azure: %s", err) + } + + name := d.Get("name").(string) + + // search for the DNS server's definition: + for _, dns := range netConf.Configuration.DNS.DNSServers { + if dns.Name == name { + return true, nil + } + } + + // if we reached this point; the resource must have been deleted; and we must untrack it: + d.SetId("") + return false, nil +} + +// resourceAzureDnsServerDelete does all the necessary API calls +// to delete the DNS server definition from Azure. +func resourceAzureDnsServerDelete(d *schema.ResourceData, meta interface{}) error { + azureClient := meta.(*Client) + mgmtClient := azureClient.mgmtClient + networkClient := virtualnetwork.NewClient(mgmtClient) + + log.Println("[INFO] Fetching current network configuration from Azure.") + azureClient.mutex.Lock() + defer azureClient.mutex.Unlock() + netConf, err := networkClient.GetVirtualNetworkConfiguration() + if err != nil { + return fmt.Errorf("Failed to get the current network configuration from Azure: %s", err) + } + + name := d.Get("name").(string) + + // search for the DNS server's definition and remove it: + var found bool + for i, dns := range netConf.Configuration.DNS.DNSServers { + if dns.Name == name { + found = true + netConf.Configuration.DNS.DNSServers = append( + netConf.Configuration.DNS.DNSServers[:i], + netConf.Configuration.DNS.DNSServers[i+1:]..., + ) + break + } + } + + // if not found; don't bother re-sending the natwork config: + if !found { + return nil + } + + // send the configuration back to Azure: + log.Println("[INFO] Sending updated network configuration back to Azure.") + reqID, err := networkClient.SetVirtualNetworkConfiguration(netConf) + if err != nil { + return fmt.Errorf("Failed issuing update to network configuration: %s", err) + } + err = mgmtClient.WaitForOperation(reqID, nil) + if err != nil { + return fmt.Errorf("Error setting network configuration: %s", err) + } + + return nil +} diff --git a/builtin/providers/azure/resource_azure_dns_server_test.go b/builtin/providers/azure/resource_azure_dns_server_test.go new file mode 100644 index 0000000000..1654f7ee81 --- /dev/null +++ b/builtin/providers/azure/resource_azure_dns_server_test.go @@ -0,0 +1,128 @@ +package azure + +import ( + "fmt" + "testing" + + "github.com/Azure/azure-sdk-for-go/management/virtualnetwork" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestAccAzureDnsServerBasic(t *testing.T) { + name := "azure_dns_server.foo" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAzureDnsServerDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccAzureDnsServerBasic, + Check: resource.ComposeTestCheckFunc( + testAccCheckAzureDnsServerExists(name), + resource.TestCheckResourceAttr(name, "name", "terraform-dns-server"), + resource.TestCheckResourceAttr(name, "dns_address", "8.8.8.8"), + ), + }, + }, + }) +} + +func TestAccAzureDnsServerUpdate(t *testing.T) { + name := "azure_dns_server.foo" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAzureDnsServerDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccAzureDnsServerBasic, + Check: resource.ComposeTestCheckFunc( + testAccCheckAzureDnsServerExists(name), + resource.TestCheckResourceAttr(name, "name", "terraform-dns-server"), + resource.TestCheckResourceAttr(name, "dns_address", "8.8.8.8"), + ), + }, + + resource.TestStep{ + Config: testAccAzureDnsServerUpdate, + Check: resource.ComposeTestCheckFunc( + testAccCheckAzureDnsServerExists(name), + resource.TestCheckResourceAttr(name, "name", "terraform-dns-server"), + resource.TestCheckResourceAttr(name, "dns_address", "8.8.4.4"), + ), + }, + }, + }) +} + +func testAccCheckAzureDnsServerExists(name string) resource.TestCheckFunc { + return func(s *terraform.State) error { + resource, ok := s.RootModule().Resources[name] + if !ok { + return fmt.Errorf("Resource not found: %s", name) + } + + if resource.Primary.ID == "" { + return fmt.Errorf("No DNS Server ID set.") + } + + mgmtClient := testAccProvider.Meta().(*Client).mgmtClient + netConf, err := virtualnetwork.NewClient(mgmtClient).GetVirtualNetworkConfiguration() + if err != nil { + return fmt.Errorf("Failed fetching networking configuration: %s", err) + } + + for _, dns := range netConf.Configuration.DNS.DNSServers { + if dns.Name == resource.Primary.ID { + return nil + } + } + + return fmt.Errorf("Azure DNS Server not found.") + } +} + +func testAccCheckAzureDnsServerDestroy(s *terraform.State) error { + mgmtClient := testAccProvider.Meta().(*Client).mgmtClient + + for _, resource := range s.RootModule().Resources { + if resource.Type != "azure_dns_server" { + continue + } + + if resource.Primary.ID == "" { + return fmt.Errorf("No DNS Server ID is set.") + } + + networkClient := virtualnetwork.NewClient(mgmtClient) + netConf, err := networkClient.GetVirtualNetworkConfiguration() + if err != nil { + return fmt.Errorf("Error retrieving networking configuration from Azure: %s", err) + } + + for _, dns := range netConf.Configuration.DNS.DNSServers { + if dns.Name == resource.Primary.ID { + return fmt.Errorf("Azure DNS Server still exists.") + } + } + } + + return nil +} + +const testAccAzureDnsServerBasic = ` +resource "azure_dns_server" "foo" { + name = "terraform-dns-server" + dns_address = "8.8.8.8" +} +` + +const testAccAzureDnsServerUpdate = ` +resource "azure_dns_server" "foo" { + name = "terraform-dns-server" + dns_address = "8.8.4.4" +} +` diff --git a/builtin/providers/azure/resource_azure_hosted_service.go b/builtin/providers/azure/resource_azure_hosted_service.go new file mode 100644 index 0000000000..6fb2d956da --- /dev/null +++ b/builtin/providers/azure/resource_azure_hosted_service.go @@ -0,0 +1,171 @@ +package azure + +import ( + "encoding/base64" + "fmt" + "log" + + "github.com/Azure/azure-sdk-for-go/management" + "github.com/Azure/azure-sdk-for-go/management/hostedservice" + "github.com/hashicorp/terraform/helper/schema" +) + +// resourceAzureHostedService returns the schema.Resource associated to an +// Azure hosted service. +func resourceAzureHostedService() *schema.Resource { + return &schema.Resource{ + Create: resourceAzureHostedServiceCreate, + Read: resourceAzureHostedServiceRead, + Update: resourceAzureHostedServiceUpdate, + Delete: resourceAzureHostedServiceDelete, + + Schema: map[string]*schema.Schema{ + "name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: parameterDescriptions["name"], + }, + "location": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: parameterDescriptions["location"], + }, + "ephemeral_contents": &schema.Schema{ + Type: schema.TypeBool, + Required: true, + Description: parameterDescriptions["ephemeral_contents"], + }, + "url": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + "status": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + "reverse_dns_fqdn": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Description: parameterDescriptions["reverse_dns_fqdn"], + }, + "label": &schema.Schema{ + Type: schema.TypeString, + ForceNew: true, + Optional: true, + Default: "Made by Terraform.", + Description: parameterDescriptions["label"], + }, + "description": &schema.Schema{ + Type: schema.TypeString, + ForceNew: true, + Optional: true, + Description: parameterDescriptions["description"], + }, + "default_certificate_thumbprint": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + Optional: true, + Description: parameterDescriptions["default_certificate_thumbprint"], + }, + }, + } +} + +// resourceAzureHostedServiceCreate does all the necessary API calls +// to create a hosted service on Azure. +func resourceAzureHostedServiceCreate(d *schema.ResourceData, meta interface{}) error { + azureClient := meta.(*Client) + mgmtClient := azureClient.mgmtClient + hostedServiceClient := hostedservice.NewClient(mgmtClient) + + serviceName := d.Get("name").(string) + location := d.Get("location").(string) + reverseDNS := d.Get("reverse_dns_fqdn").(string) + description := d.Get("description").(string) + label := base64.StdEncoding.EncodeToString([]byte(d.Get("label").(string))) + + err := hostedServiceClient.CreateHostedService( + hostedservice.CreateHostedServiceParameters{ + ServiceName: serviceName, + Location: location, + Label: label, + Description: description, + ReverseDNSFqdn: reverseDNS, + }, + ) + if err != nil { + return fmt.Errorf("Failed defining new Azure hosted service: %s", err) + } + + d.SetId(serviceName) + return nil +} + +// resourceAzureHostedServiceRead does all the necessary API calls +// to read the state of a hosted service from Azure. +func resourceAzureHostedServiceRead(d *schema.ResourceData, meta interface{}) error { + azureClient := meta.(*Client) + hostedServiceClient := hostedservice.NewClient(azureClient.mgmtClient) + + log.Println("[INFO] Querying for hosted service info.") + serviceName := d.Get("name").(string) + hostedService, err := hostedServiceClient.GetHostedService(serviceName) + if err != nil { + if management.IsResourceNotFoundError(err) { + // it means the hosted service was deleted in the meantime, + // so we must remove it here: + d.SetId("") + return nil + } else { + return fmt.Errorf("Failed to get hosted service: %s", err) + } + } + + log.Println("[DEBUG] Reading hosted service query result data.") + d.Set("name", hostedService.ServiceName) + d.Set("url", hostedService.URL) + d.Set("location", hostedService.Location) + d.Set("description", hostedService.Description) + d.Set("label", hostedService.Label) + d.Set("status", hostedService.Status) + d.Set("reverse_dns_fqdn", hostedService.ReverseDNSFqdn) + d.Set("default_certificate_thumbprint", hostedService.DefaultWinRmCertificateThumbprint) + + return nil +} + +// resourceAzureHostedServiceUpdate does all the necessary API calls to +// update some settings of a hosted service on Azure. +func resourceAzureHostedServiceUpdate(d *schema.ResourceData, meta interface{}) error { + // NOTE: although no-op; this is still required in order for updates to + // ephemeral_contents to be possible. + + // check if the service still exists: + return resourceAzureHostedServiceRead(d, meta) +} + +// resourceAzureHostedServiceDelete does all the necessary API calls to +// delete a hosted service from Azure. +func resourceAzureHostedServiceDelete(d *schema.ResourceData, meta interface{}) error { + azureClient := meta.(*Client) + mgmtClient := azureClient.mgmtClient + hostedServiceClient := hostedservice.NewClient(mgmtClient) + + log.Println("[INFO] Issuing hosted service deletion.") + serviceName := d.Get("name").(string) + ephemeral := d.Get("ephemeral_contents").(bool) + reqID, err := hostedServiceClient.DeleteHostedService(serviceName, ephemeral) + if err != nil { + return fmt.Errorf("Failed issuing hosted service deletion request: %s", err) + } + + log.Println("[DEBUG] Awaiting confirmation on hosted service deletion.") + err = mgmtClient.WaitForOperation(reqID, nil) + if err != nil { + return fmt.Errorf("Error on hosted service deletion: %s", err) + } + + return nil +} diff --git a/builtin/providers/azure/resource_azure_hosted_service_test.go b/builtin/providers/azure/resource_azure_hosted_service_test.go new file mode 100644 index 0000000000..f0ef50e89e --- /dev/null +++ b/builtin/providers/azure/resource_azure_hosted_service_test.go @@ -0,0 +1,124 @@ +package azure + +import ( + "fmt" + "testing" + + "github.com/Azure/azure-sdk-for-go/management/hostedservice" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestAccAzureHostedServiceBasic(t *testing.T) { + name := "azure_hosted_service.foo" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAzureHostedServiceDestroyed, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccAzureHostedServiceBasic, + Check: resource.ComposeTestCheckFunc( + testAccCheckAzureHostedServiceExists(name), + resource.TestCheckResourceAttr(name, "name", "terraform-testing-service"), + resource.TestCheckResourceAttr(name, "location", "North Europe"), + resource.TestCheckResourceAttr(name, "ephemeral_contents", "false"), + resource.TestCheckResourceAttr(name, "description", "very discriptive"), + resource.TestCheckResourceAttr(name, "label", "very identifiable"), + ), + }, + }, + }) +} + +func TestAccAzureHostedServiceUpdate(t *testing.T) { + name := "azure_hosted_service.foo" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAzureHostedServiceDestroyed, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccAzureHostedServiceBasic, + Check: resource.ComposeTestCheckFunc( + testAccCheckAzureHostedServiceExists(name), + resource.TestCheckResourceAttr(name, "name", "terraform-testing-service"), + resource.TestCheckResourceAttr(name, "location", "North Europe"), + resource.TestCheckResourceAttr(name, "ephemeral_contents", "false"), + resource.TestCheckResourceAttr(name, "description", "very discriptive"), + resource.TestCheckResourceAttr(name, "label", "very identifiable"), + ), + }, + + resource.TestStep{ + Config: testAccAzureHostedServiceUpdate, + Check: resource.ComposeTestCheckFunc( + testAccCheckAzureHostedServiceExists(name), + resource.TestCheckResourceAttr(name, "name", "terraform-testing-service"), + resource.TestCheckResourceAttr(name, "location", "North Europe"), + resource.TestCheckResourceAttr(name, "ephemeral_contents", "true"), + resource.TestCheckResourceAttr(name, "description", "very discriptive"), + resource.TestCheckResourceAttr(name, "label", "very identifiable"), + ), + }, + }, + }) +} + +func testAccCheckAzureHostedServiceExists(name string) resource.TestCheckFunc { + return func(s *terraform.State) error { + resource, ok := s.RootModule().Resources[name] + if !ok { + return fmt.Errorf("Hosted Service resource not found.") + } + + if resource.Primary.ID == "" { + return fmt.Errorf("Resource's ID is not set.") + } + + mgmtClient := testAccProvider.Meta().(*Client).mgmtClient + _, err := hostedservice.NewClient(mgmtClient).GetHostedService(resource.Primary.ID) + return err + } +} + +func testAccCheckAzureHostedServiceDestroyed(s *terraform.State) error { + mgmtClient := testAccProvider.Meta().(*Client).mgmtClient + + for _, resource := range s.RootModule().Resources { + if resource.Type != "azure_hosted_service" { + continue + } + + if resource.Primary.ID == "" { + return fmt.Errorf("No Azure Hosted Service Resource found.") + } + + _, err := hostedservice.NewClient(mgmtClient).GetHostedService(resource.Primary.ID) + + return testAccResourceDestroyedErrorFilter("Hosted Service", err) + } + + return nil +} + +const testAccAzureHostedServiceBasic = ` +resource "azure_hosted_service" "foo" { + name = "terraform-testing-service" + location = "North Europe" + ephemeral_contents = false + description = "very discriptive" + label = "very identifiable" +} +` +const testAccAzureHostedServiceUpdate = ` +resource "azure_hosted_service" "foo" { + name = "terraform-testing-service" + location = "North Europe" + ephemeral_contents = true + description = "very discriptive" + label = "very identifiable" +} +` diff --git a/builtin/providers/azure/resource_azure_instance.go b/builtin/providers/azure/resource_azure_instance.go index ad8a77a9cc..967e49dce8 100644 --- a/builtin/providers/azure/resource_azure_instance.go +++ b/builtin/providers/azure/resource_azure_instance.go @@ -7,14 +7,14 @@ import ( "log" "strings" + "github.com/Azure/azure-sdk-for-go/management" + "github.com/Azure/azure-sdk-for-go/management/hostedservice" + "github.com/Azure/azure-sdk-for-go/management/osimage" + "github.com/Azure/azure-sdk-for-go/management/virtualmachine" + "github.com/Azure/azure-sdk-for-go/management/virtualmachineimage" + "github.com/Azure/azure-sdk-for-go/management/vmutils" "github.com/hashicorp/terraform/helper/hashcode" "github.com/hashicorp/terraform/helper/schema" - "github.com/svanharmelen/azure-sdk-for-go/management" - "github.com/svanharmelen/azure-sdk-for-go/management/hostedservice" - "github.com/svanharmelen/azure-sdk-for-go/management/osimage" - "github.com/svanharmelen/azure-sdk-for-go/management/virtualmachine" - "github.com/svanharmelen/azure-sdk-for-go/management/virtualmachineimage" - "github.com/svanharmelen/azure-sdk-for-go/management/vmutils" ) const ( @@ -68,7 +68,7 @@ func resourceAzureInstance() *schema.Resource { ForceNew: true, }, - "storage": &schema.Schema{ + "storage_service_name": &schema.Schema{ Type: schema.TypeString, Optional: true, ForceNew: true, @@ -183,7 +183,7 @@ func resourceAzureInstanceCreate(d *schema.ResourceData, meta interface{}) (err mc, d.Get("image").(string), name, - d.Get("storage").(string), + d.Get("storage_service_name").(string), ) if err != nil { return err diff --git a/builtin/providers/azure/resource_azure_instance_test.go b/builtin/providers/azure/resource_azure_instance_test.go index 590399dfd1..c474618307 100644 --- a/builtin/providers/azure/resource_azure_instance_test.go +++ b/builtin/providers/azure/resource_azure_instance_test.go @@ -5,11 +5,11 @@ import ( "os" "testing" + "github.com/Azure/azure-sdk-for-go/management" + "github.com/Azure/azure-sdk-for-go/management/hostedservice" + "github.com/Azure/azure-sdk-for-go/management/virtualmachine" "github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/terraform" - "github.com/svanharmelen/azure-sdk-for-go/management" - "github.com/svanharmelen/azure-sdk-for-go/management/hostedservice" - "github.com/svanharmelen/azure-sdk-for-go/management/virtualmachine" ) func TestAccAzureInstance_basic(t *testing.T) { @@ -313,7 +313,7 @@ resource "azure_instance" "foo" { name = "terraform-test" image = "Ubuntu Server 14.04 LTS" size = "Basic_A1" - storage = "%s" + storage_service_name = "%s" location = "West US" username = "terraform" password = "Pass!admin123" @@ -324,7 +324,7 @@ resource "azure_instance" "foo" { public_port = 22 private_port = 22 } -}`, os.Getenv("AZURE_STORAGE")) +}`, testAccStorageServiceName) var testAccAzureInstance_advanced = fmt.Sprintf(` resource "azure_virtual_network" "foo" { @@ -346,23 +346,24 @@ resource "azure_virtual_network" "foo" { resource "azure_security_group" "foo" { name = "terraform-security-group1" location = "West US" +} - rule { - name = "rdp" - priority = 101 - source_cidr = "*" - source_port = "*" - destination_cidr = "*" - destination_port = 3389 - protocol = "TCP" - } +resource "azure_security_group_rule" "foo" { + name = "rdp" + security_group_name = "${azure_security_group.foo.name}" + priority = 101 + source_address_prefix = "*" + source_port_range = "*" + destination_address_prefix = "*" + destination_port_range = "3389" + protocol = "TCP" } resource "azure_instance" "foo" { name = "terraform-test1" image = "Windows Server 2012 R2 Datacenter, April 2015" size = "Basic_A1" - storage = "%s" + storage_service_name = "%s" location = "West US" time_zone = "America/Los_Angeles" subnet = "subnet1" @@ -377,7 +378,7 @@ resource "azure_instance" "foo" { public_port = 3389 private_port = 3389 } -}`, os.Getenv("AZURE_STORAGE")) +}`, testAccStorageServiceName) var testAccAzureInstance_update = fmt.Sprintf(` resource "azure_virtual_network" "foo" { @@ -385,52 +386,54 @@ resource "azure_virtual_network" "foo" { address_space = ["10.1.2.0/24"] location = "West US" - subnet { + subnet { name = "subnet1" - address_prefix = "10.1.2.0/25" - } + address_prefix = "10.1.2.0/25" + } - subnet { + subnet { name = "subnet2" - address_prefix = "10.1.2.128/25" + address_prefix = "10.1.2.128/25" } } resource "azure_security_group" "foo" { name = "terraform-security-group1" location = "West US" +} - rule { - name = "rdp" - priority = 101 - source_cidr = "*" - source_port = "*" - destination_cidr = "*" - destination_port = 3389 - protocol = "TCP" - } +resource "azure_security_group_rule" "foo" { + name = "rdp" + security_group_name = "${azure_security_group.foo.name}" + priority = 101 + source_address_prefix = "*" + source_port_range = "*" + destination_address_prefix = "*" + destination_port_range = "3389" + protocol = "TCP" } resource "azure_security_group" "bar" { name = "terraform-security-group2" location = "West US" +} - rule { - name = "rdp" - priority = 101 - source_cidr = "192.168.0.0/24" - source_port = "*" - destination_cidr = "*" - destination_port = 3389 - protocol = "TCP" - } +resource "azure_security_group_rule" "bar" { + name = "rdp" + security_group_name = "${azure_security_group.bar.name}" + priority = 101 + source_address_prefix = "192.168.0.0/24" + source_port_range = "*" + destination_address_prefix = "*" + destination_port_range = "3389" + protocol = "TCP" } resource "azure_instance" "foo" { name = "terraform-test1" image = "Windows Server 2012 R2 Datacenter, April 2015" size = "Basic_A2" - storage = "%s" + storage_service_name = "%s" location = "West US" time_zone = "America/Los_Angeles" subnet = "subnet1" @@ -452,4 +455,4 @@ resource "azure_instance" "foo" { public_port = 5985 private_port = 5985 } -}`, os.Getenv("AZURE_STORAGE")) +}`, testAccStorageServiceName) diff --git a/builtin/providers/azure/resource_azure_local_network.go b/builtin/providers/azure/resource_azure_local_network.go new file mode 100644 index 0000000000..cfb354fca0 --- /dev/null +++ b/builtin/providers/azure/resource_azure_local_network.go @@ -0,0 +1,265 @@ +package azure + +import ( + "fmt" + "log" + + "github.com/Azure/azure-sdk-for-go/management/virtualnetwork" + "github.com/hashicorp/terraform/helper/schema" +) + +// resourceAzureLocalNetworkConnetion returns the schema.Resource associated to an +// Azure hosted service. +func resourceAzureLocalNetworkConnection() *schema.Resource { + return &schema.Resource{ + Create: resourceAzureLocalNetworkConnectionCreate, + Read: resourceAzureLocalNetworkConnectionRead, + Update: resourceAzureLocalNetworkConnectionUpdate, + Exists: resourceAzureLocalNetworkConnectionExists, + Delete: resourceAzureLocalNetworkConnectionDelete, + + Schema: map[string]*schema.Schema{ + "name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: parameterDescriptions["name"], + }, + "vpn_gateway_address": &schema.Schema{ + Type: schema.TypeString, + Required: true, + Description: parameterDescriptions["vpn_gateway_address"], + }, + "address_space_prefixes": &schema.Schema{ + Type: schema.TypeList, + Required: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + Description: parameterDescriptions["address_space_prefixes"], + }, + }, + } +} + +// sourceAzureLocalNetworkConnectionCreate issues all the necessary API calls +// to create a virtual network on Azure. +func resourceAzureLocalNetworkConnectionCreate(d *schema.ResourceData, meta interface{}) error { + azureClient, ok := meta.(*Client) + if !ok { + return fmt.Errorf("Failed to convert to *Client, got: %T", meta) + } + mgmtClient := azureClient.mgmtClient + networkClient := virtualnetwork.NewClient(mgmtClient) + + log.Println("[INFO] Fetching current network configuration from Azure.") + azureClient.mutex.Lock() + defer azureClient.mutex.Unlock() + netConf, err := networkClient.GetVirtualNetworkConfiguration() + if err != nil { + return fmt.Errorf("Failed to get the current network configuration from Azure: %s", err) + } + + // get provided configuration: + name := d.Get("name").(string) + vpnGateway := d.Get("vpn_gateway_address").(string) + var prefixes []string + for _, prefix := range d.Get("address_space_prefixes").([]interface{}) { + prefixes = append(prefixes, prefix.(string)) + } + + // add configuration to network config: + netConf.Configuration.LocalNetworkSites = append(netConf.Configuration.LocalNetworkSites, + virtualnetwork.LocalNetworkSite{ + Name: name, + VPNGatewayAddress: vpnGateway, + AddressSpace: virtualnetwork.AddressSpace{ + AddressPrefix: prefixes, + }, + }) + + // send the configuration back to Azure: + log.Println("[INFO] Sending updated network configuration back to Azure.") + reqID, err := networkClient.SetVirtualNetworkConfiguration(netConf) + if err != nil { + return fmt.Errorf("Failed setting updated network configuration: %s", err) + } + err = mgmtClient.WaitForOperation(reqID, nil) + if err != nil { + return fmt.Errorf("Failed updating the network configuration: %s", err) + } + + d.SetId(name) + return nil +} + +// resourceAzureLocalNetworkConnectionRead does all the necessary API calls to +// read the state of our local natwork from Azure. +func resourceAzureLocalNetworkConnectionRead(d *schema.ResourceData, meta interface{}) error { + azureClient, ok := meta.(*Client) + if !ok { + return fmt.Errorf("Failed to convert to *Client, got: %T", meta) + } + mgmtClient := azureClient.mgmtClient + networkClient := virtualnetwork.NewClient(mgmtClient) + + log.Println("[INFO] Fetching current network configuration from Azure.") + netConf, err := networkClient.GetVirtualNetworkConfiguration() + if err != nil { + return fmt.Errorf("Failed to get the current network configuration from Azure: %s", err) + } + + var found bool + name := d.Get("name").(string) + + // browsing for our network config: + for _, lnet := range netConf.Configuration.LocalNetworkSites { + if lnet.Name == name { + found = true + d.Set("vpn_gateway_address", lnet.VPNGatewayAddress) + d.Set("address_space_prefixes", lnet.AddressSpace.AddressPrefix) + break + } + } + + // remove the resource from the state of it has been deleted in the meantime: + if !found { + log.Println(fmt.Printf("[INFO] Azure local network '%s' has been deleted remotely. Removimg from Terraform.", name)) + d.SetId("") + } + + return nil +} + +// resourceAzureLocalNetworkConnectionUpdate does all the necessary API calls +// update the settings of our Local Network on Azure. +func resourceAzureLocalNetworkConnectionUpdate(d *schema.ResourceData, meta interface{}) error { + azureClient, ok := meta.(*Client) + if !ok { + return fmt.Errorf("Failed to convert to *Client, got: %T", meta) + } + mgmtClient := azureClient.mgmtClient + networkClient := virtualnetwork.NewClient(mgmtClient) + + log.Println("[INFO] Fetching current network configuration from Azure.") + azureClient.mutex.Lock() + defer azureClient.mutex.Unlock() + netConf, err := networkClient.GetVirtualNetworkConfiguration() + if err != nil { + return fmt.Errorf("Failed to get the current network configuration from Azure: %s", err) + } + + name := d.Get("name").(string) + cvpn := d.HasChange("vpn_gateway_address") + cprefixes := d.HasChange("address_space_prefixes") + + var found bool + for i, lnet := range netConf.Configuration.LocalNetworkSites { + if lnet.Name == name { + found = true + if cvpn { + netConf.Configuration.LocalNetworkSites[i].VPNGatewayAddress = d.Get("vpn_gateway_address").(string) + } + if cprefixes { + var prefixes []string + for _, prefix := range d.Get("address_space_prefixes").([]interface{}) { + prefixes = append(prefixes, prefix.(string)) + } + netConf.Configuration.LocalNetworkSites[i].AddressSpace.AddressPrefix = prefixes + } + break + } + } + + // remove the resource from the state of it has been deleted in the meantime: + if !found { + log.Println(fmt.Printf("[INFO] Azure local network '%s' has been deleted remotely. Removimg from Terraform.", name)) + d.SetId("") + } else if cvpn || cprefixes { + // else, send the configuration back to Azure: + log.Println("[INFO] Sending updated network configuration back to Azure.") + reqID, err := networkClient.SetVirtualNetworkConfiguration(netConf) + if err != nil { + return fmt.Errorf("Failed setting updated network configuration: %s", err) + } + err = mgmtClient.WaitForOperation(reqID, nil) + if err != nil { + return fmt.Errorf("Failed updating the network configuration: %s", err) + } + } + + return nil +} + +// resourceAzureLocalNetworkConnectionExists does all the necessary API calls +// to check if the local network already exists on Azure. +func resourceAzureLocalNetworkConnectionExists(d *schema.ResourceData, meta interface{}) (bool, error) { + azureClient, ok := meta.(*Client) + if !ok { + return false, fmt.Errorf("Failed to convert to *Client, got: %T", meta) + } + mgmtClient := azureClient.mgmtClient + networkClient := virtualnetwork.NewClient(mgmtClient) + + log.Println("[INFO] Fetching current network configuration from Azure.") + netConf, err := networkClient.GetVirtualNetworkConfiguration() + if err != nil { + return false, fmt.Errorf("Failed to get the current network configuration from Azure: %s", err) + } + + name := d.Get("name") + + for _, lnet := range netConf.Configuration.LocalNetworkSites { + if lnet.Name == name { + return true, nil + } + } + + return false, nil +} + +// resourceAzureLocalNetworkConnectionDelete does all the necessary API calls +// to delete a local network off Azure. +func resourceAzureLocalNetworkConnectionDelete(d *schema.ResourceData, meta interface{}) error { + azureClient, ok := meta.(*Client) + if !ok { + return fmt.Errorf("Failed to convert to *Client, got: %T", meta) + } + mgmtClient := azureClient.mgmtClient + networkClient := virtualnetwork.NewClient(mgmtClient) + + log.Println("[INFO] Fetching current network configuration from Azure.") + azureClient.mutex.Lock() + defer azureClient.mutex.Unlock() + netConf, err := networkClient.GetVirtualNetworkConfiguration() + if err != nil { + return fmt.Errorf("Failed to get the current network configuration from Azure: %s", err) + } + + name := d.Get("name").(string) + + // search for our local network and remove it if found: + for i, lnet := range netConf.Configuration.LocalNetworkSites { + if lnet.Name == name { + netConf.Configuration.LocalNetworkSites = append( + netConf.Configuration.LocalNetworkSites[:i], + netConf.Configuration.LocalNetworkSites[i+1:]..., + ) + break + } + } + + // send the configuration back to Azure: + log.Println("[INFO] Sending updated network configuration back to Azure.") + reqID, err := networkClient.SetVirtualNetworkConfiguration(netConf) + if err != nil { + return fmt.Errorf("Failed setting updated network configuration: %s", err) + } + err = mgmtClient.WaitForOperation(reqID, nil) + if err != nil { + return fmt.Errorf("Failed updating the network configuration: %s", err) + } + + d.SetId("") + return nil +} diff --git a/builtin/providers/azure/resource_azure_local_network_test.go b/builtin/providers/azure/resource_azure_local_network_test.go new file mode 100644 index 0000000000..ae120569a0 --- /dev/null +++ b/builtin/providers/azure/resource_azure_local_network_test.go @@ -0,0 +1,140 @@ +package azure + +import ( + "fmt" + "testing" + + "github.com/Azure/azure-sdk-for-go/management/virtualnetwork" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestAccAzureLocalNetworkConnectionBasic(t *testing.T) { + name := "azure_local_network_connection.foo" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccAzureLocalNetworkConnectionDestroyed, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccAzureLocalNetworkConnectionBasic, + Check: resource.ComposeTestCheckFunc( + testAccAzureLocalNetworkConnectionExists(name), + resource.TestCheckResourceAttr(name, "name", "terraform-local-network-connection"), + resource.TestCheckResourceAttr(name, "vpn_gateway_address", "10.11.12.13"), + resource.TestCheckResourceAttr(name, "address_space_prefixes.0", "10.10.10.0/31"), + resource.TestCheckResourceAttr(name, "address_space_prefixes.1", "10.10.10.1/31"), + ), + }, + }, + }) +} + +func TestAccAzureLocalNetworkConnectionUpdate(t *testing.T) { + name := "azure_local_network_connection.foo" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccAzureLocalNetworkConnectionDestroyed, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccAzureLocalNetworkConnectionBasic, + Check: resource.ComposeTestCheckFunc( + testAccAzureLocalNetworkConnectionExists(name), + resource.TestCheckResourceAttr(name, "name", "terraform-local-network-connection"), + resource.TestCheckResourceAttr(name, "vpn_gateway_address", "10.11.12.13"), + resource.TestCheckResourceAttr(name, "address_space_prefixes.0", "10.10.10.0/31"), + resource.TestCheckResourceAttr(name, "address_space_prefixes.1", "10.10.10.1/31"), + ), + }, + + resource.TestStep{ + Config: testAccAzureLocalNetworkConnectionUpdate, + Check: resource.ComposeTestCheckFunc( + testAccAzureLocalNetworkConnectionExists(name), + resource.TestCheckResourceAttr(name, "name", "terraform-local-network-connection"), + resource.TestCheckResourceAttr(name, "vpn_gateway_address", "10.11.12.14"), + resource.TestCheckResourceAttr(name, "address_space_prefixes.0", "10.10.10.2/30"), + resource.TestCheckResourceAttr(name, "address_space_prefixes.1", "10.10.10.3/30"), + ), + }, + }, + }) +} + +// testAccAzureLocalNetworkConnectionExists checks whether the given local network +// connection exists on Azure. +func testAccAzureLocalNetworkConnectionExists(name string) resource.TestCheckFunc { + return func(s *terraform.State) error { + resource, ok := s.RootModule().Resources[name] + if !ok { + return fmt.Errorf("Azure Local Network Connection not found: %s", name) + } + + if resource.Primary.ID == "" { + return fmt.Errorf("Azure Local Network Connection ID not set.") + } + + mgmtClient := testAccProvider.Meta().(*Client).mgmtClient + netConf, err := virtualnetwork.NewClient(mgmtClient).GetVirtualNetworkConfiguration() + if err != nil { + return err + } + + for _, lnet := range netConf.Configuration.LocalNetworkSites { + if lnet.Name == resource.Primary.ID { + return nil + } + break + } + + return fmt.Errorf("Local Network Connection not found: %s", name) + } +} + +// testAccAzureLocalNetworkConnectionDestroyed checks whether the local network +// connection has been destroyed on Azure or not. +func testAccAzureLocalNetworkConnectionDestroyed(s *terraform.State) error { + mgmtClient := testAccProvider.Meta().(*Client).mgmtClient + + for _, resource := range s.RootModule().Resources { + if resource.Type != "azure_local_network_connection" { + continue + } + + if resource.Primary.ID == "" { + return fmt.Errorf("Azure Local Network Connection ID not set.") + } + + netConf, err := virtualnetwork.NewClient(mgmtClient).GetVirtualNetworkConfiguration() + if err != nil { + return err + } + + for _, lnet := range netConf.Configuration.LocalNetworkSites { + if lnet.Name == resource.Primary.ID { + return fmt.Errorf("Azure Local Network Connection still exists.") + } + } + } + + return nil +} + +const testAccAzureLocalNetworkConnectionBasic = ` +resource "azure_local_network_connection" "foo" { + name = "terraform-local-network-connection" + vpn_gateway_address = "10.11.12.13" + address_space_prefixes = ["10.10.10.0/31", "10.10.10.1/31"] +} +` + +const testAccAzureLocalNetworkConnectionUpdate = ` +resource "azure_local_network_connection" "foo" { + name = "terraform-local-network-connection" + vpn_gateway_address = "10.11.12.14" + address_space_prefixes = ["10.10.10.2/30", "10.10.10.3/30"] +} +` diff --git a/builtin/providers/azure/resource_azure_security_group.go b/builtin/providers/azure/resource_azure_security_group.go index 842b124340..4b2b8b6de8 100644 --- a/builtin/providers/azure/resource_azure_security_group.go +++ b/builtin/providers/azure/resource_azure_security_group.go @@ -1,22 +1,18 @@ package azure import ( - "bytes" "fmt" "log" - "strconv" - "github.com/hashicorp/terraform/helper/hashcode" + "github.com/Azure/azure-sdk-for-go/management" + "github.com/Azure/azure-sdk-for-go/management/networksecuritygroup" "github.com/hashicorp/terraform/helper/schema" - "github.com/svanharmelen/azure-sdk-for-go/management" - "github.com/svanharmelen/azure-sdk-for-go/management/networksecuritygroup" ) func resourceAzureSecurityGroup() *schema.Resource { return &schema.Resource{ Create: resourceAzureSecurityGroupCreate, Read: resourceAzureSecurityGroupRead, - Update: resourceAzureSecurityGroupUpdate, Delete: resourceAzureSecurityGroupDelete, Schema: map[string]*schema.Schema{ @@ -38,63 +34,6 @@ func resourceAzureSecurityGroup() *schema.Resource { Required: true, ForceNew: true, }, - - "rule": &schema.Schema{ - Type: schema.TypeSet, - Required: true, - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "name": &schema.Schema{ - Type: schema.TypeString, - Required: true, - }, - - "type": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - Default: "Inbound", - }, - - "priority": &schema.Schema{ - Type: schema.TypeInt, - Required: true, - }, - - "action": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - Default: "Allow", - }, - - "source_cidr": &schema.Schema{ - Type: schema.TypeString, - Required: true, - }, - - "source_port": &schema.Schema{ - Type: schema.TypeString, - Required: true, - }, - - "destination_cidr": &schema.Schema{ - Type: schema.TypeString, - Required: true, - }, - - "destination_port": &schema.Schema{ - Type: schema.TypeString, - Required: true, - }, - - "protocol": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - Default: "TCP", - }, - }, - }, - Set: resourceAzureSecurityGroupRuleHash, - }, }, } } @@ -126,70 +65,9 @@ func resourceAzureSecurityGroupCreate(d *schema.ResourceData, meta interface{}) d.SetId(name) - // Create all rules that are configured - if rs := d.Get("rule").(*schema.Set); rs.Len() > 0 { - - // Create an empty schema.Set to hold all rules - rules := &schema.Set{ - F: resourceAzureSecurityGroupRuleHash, - } - - for _, rule := range rs.List() { - // Create a single rule - err := resourceAzureSecurityGroupRuleCreate(d, meta, rule.(map[string]interface{})) - - // We need to update this first to preserve the correct state - rules.Add(rule) - d.Set("rule", rules) - - if err != nil { - return err - } - } - } - return resourceAzureSecurityGroupRead(d, meta) } -func resourceAzureSecurityGroupRuleCreate( - d *schema.ResourceData, - meta interface{}, - rule map[string]interface{}) error { - mc := meta.(*Client).mgmtClient - - // Make sure all required parameters are there - if err := verifySecurityGroupRuleParams(rule); err != nil { - return err - } - - name := rule["name"].(string) - - // Create the rule - req, err := networksecuritygroup.NewClient(mc).SetNetworkSecurityGroupRule(d.Id(), - networksecuritygroup.RuleRequest{ - Name: name, - Type: networksecuritygroup.RuleType(rule["type"].(string)), - Priority: rule["priority"].(int), - Action: networksecuritygroup.RuleAction(rule["action"].(string)), - SourceAddressPrefix: rule["source_cidr"].(string), - SourcePortRange: rule["source_port"].(string), - DestinationAddressPrefix: rule["destination_cidr"].(string), - DestinationPortRange: rule["destination_port"].(string), - Protocol: networksecuritygroup.RuleProtocol(rule["protocol"].(string)), - }, - ) - if err != nil { - return fmt.Errorf("Error creating Network Security Group rule %s: %s", name, err) - } - - if err := mc.WaitForOperation(req, nil); err != nil { - return fmt.Errorf( - "Error waiting for Network Security Group rule %s to be created: %s", name, err) - } - - return nil -} - func resourceAzureSecurityGroupRead(d *schema.ResourceData, meta interface{}) error { mc := meta.(*Client).mgmtClient @@ -205,70 +83,9 @@ func resourceAzureSecurityGroupRead(d *schema.ResourceData, meta interface{}) er d.Set("label", sg.Label) d.Set("location", sg.Location) - // Create an empty schema.Set to hold all rules - rules := &schema.Set{ - F: resourceAzureSecurityGroupRuleHash, - } - - for _, r := range sg.Rules { - if !r.IsDefault { - rule := map[string]interface{}{ - "name": r.Name, - "type": string(r.Type), - "priority": r.Priority, - "action": string(r.Action), - "source_cidr": r.SourceAddressPrefix, - "source_port": r.SourcePortRange, - "destination_cidr": r.DestinationAddressPrefix, - "destination_port": r.DestinationPortRange, - "protocol": string(r.Protocol), - } - rules.Add(rule) - } - } - - d.Set("rule", rules) - return nil } -func resourceAzureSecurityGroupUpdate(d *schema.ResourceData, meta interface{}) error { - // Check if the rule set as a whole has changed - if d.HasChange("rule") { - o, n := d.GetChange("rule") - ors := o.(*schema.Set).Difference(n.(*schema.Set)) - nrs := n.(*schema.Set).Difference(o.(*schema.Set)) - - // Now first loop through all the old rules and delete any obsolete ones - for _, rule := range ors.List() { - // Delete the rule as it no longer exists in the config - err := resourceAzureSecurityGroupRuleDelete(d, meta, rule.(map[string]interface{})) - if err != nil { - return err - } - } - - // Make sure we save the state of the currently configured rules - rules := o.(*schema.Set).Intersection(n.(*schema.Set)) - d.Set("rule", rules) - - // Then loop through al the currently configured rules and create the new ones - for _, rule := range nrs.List() { - err := resourceAzureSecurityGroupRuleCreate(d, meta, rule.(map[string]interface{})) - - // We need to update this first to preserve the correct state - rules.Add(rule) - d.Set("rule", rules) - - if err != nil { - return err - } - } - } - - return resourceAzureSecurityGroupRead(d, meta) -} - func resourceAzureSecurityGroupDelete(d *schema.ResourceData, meta interface{}) error { mc := meta.(*Client).mgmtClient @@ -288,66 +105,3 @@ func resourceAzureSecurityGroupDelete(d *schema.ResourceData, meta interface{}) return nil } - -func resourceAzureSecurityGroupRuleDelete( - d *schema.ResourceData, meta interface{}, rule map[string]interface{}) error { - mc := meta.(*Client).mgmtClient - - name := rule["name"].(string) - - // Delete the rule - req, err := networksecuritygroup.NewClient(mc).DeleteNetworkSecurityGroupRule(d.Id(), name) - if err != nil { - if management.IsResourceNotFoundError(err) { - return nil - } - return fmt.Errorf("Error deleting Network Security Group rule %s: %s", name, err) - } - - if err := mc.WaitForOperation(req, nil); err != nil { - return fmt.Errorf( - "Error waiting for Network Security Group rule %s to be deleted: %s", name, err) - } - - return nil -} - -func resourceAzureSecurityGroupRuleHash(v interface{}) int { - var buf bytes.Buffer - m := v.(map[string]interface{}) - buf.WriteString(fmt.Sprintf( - "%s-%d-%s-%s-%s-%s-%s-%s", - m["type"].(string), - m["priority"].(int), - m["action"].(string), - m["source_cidr"].(string), - m["source_port"].(string), - m["destination_cidr"].(string), - m["destination_port"].(string), - m["protocol"].(string))) - - return hashcode.String(buf.String()) -} - -func verifySecurityGroupRuleParams(rule map[string]interface{}) error { - typ := rule["type"].(string) - if typ != "Inbound" && typ != "Outbound" { - return fmt.Errorf("Parameter type only accepts 'Inbound' or 'Outbound' as values") - } - - action := rule["action"].(string) - if action != "Allow" && action != "Deny" { - return fmt.Errorf("Parameter action only accepts 'Allow' or 'Deny' as values") - } - - protocol := rule["protocol"].(string) - if protocol != "TCP" && protocol != "UDP" && protocol != "*" { - _, err := strconv.ParseInt(protocol, 0, 0) - if err != nil { - return fmt.Errorf( - "Parameter type only accepts 'TCP', 'UDP' or '*' as values") - } - } - - return nil -} diff --git a/builtin/providers/azure/resource_azure_security_group_rule.go b/builtin/providers/azure/resource_azure_security_group_rule.go new file mode 100644 index 0000000000..06f6ad3d31 --- /dev/null +++ b/builtin/providers/azure/resource_azure_security_group_rule.go @@ -0,0 +1,320 @@ +package azure + +import ( + "fmt" + "log" + + "github.com/Azure/azure-sdk-for-go/management" + netsecgroup "github.com/Azure/azure-sdk-for-go/management/networksecuritygroup" + "github.com/hashicorp/terraform/helper/schema" +) + +// resourceAzureSecurityGroupRule returns the *schema.Resource for +// a network security group rule on Azure. +func resourceAzureSecurityGroupRule() *schema.Resource { + return &schema.Resource{ + Create: resourceAzureSecurityGroupRuleCreate, + Read: resourceAzureSecurityGroupRuleRead, + Update: resourceAzureSecurityGroupRuleUpdate, + Exists: resourceAzureSecurityGroupRuleExists, + Delete: resourceAzureSecurityGroupRuleDelete, + + Schema: map[string]*schema.Schema{ + "name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: parameterDescriptions["name"], + }, + "security_group_name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: parameterDescriptions["netsecgroup_secgroup_name"], + }, + // TODO(aznashwan): update Sander's docs to remove default. + "type": &schema.Schema{ + Type: schema.TypeString, + Required: true, + Description: parameterDescriptions["netsecgroup_type"], + }, + "priority": &schema.Schema{ + Type: schema.TypeInt, + Required: true, + Description: parameterDescriptions["netsecgroup_priority"], + }, + // TODO(aznashwan): update Sander's docs to remove default. + "action": &schema.Schema{ + Type: schema.TypeString, + Required: true, + Description: parameterDescriptions["netsecgroup_action"], + }, + "source_address_prefix": &schema.Schema{ + Type: schema.TypeString, + Required: true, + Description: parameterDescriptions["netsecgroup_src_addr_prefix"], + }, + "source_port_range": &schema.Schema{ + Type: schema.TypeString, + Required: true, + Description: parameterDescriptions["netsecgroup_src_port_range"], + }, + "destination_address_prefix": &schema.Schema{ + Type: schema.TypeString, + Required: true, + Description: parameterDescriptions["netsecgroup_dest_addr_prefix"], + }, + "destination_port_range": &schema.Schema{ + Type: schema.TypeString, + Required: true, + Description: parameterDescriptions["netsecgroup_dest_port_range"], + }, + // TODO(aznashwan): update Sander's docs to remove default. + "protocol": &schema.Schema{ + Type: schema.TypeString, + Required: true, + Description: parameterDescriptions["netsecgroup_protocol"], + }, + }, + } +} + +// resourceAzureSecurityGroupRuleCreate does all the necessary API calls to +// create a new network security group rule on Azure. +func resourceAzureSecurityGroupRuleCreate(d *schema.ResourceData, meta interface{}) error { + azureClient := meta.(*Client) + mgmtClient := azureClient.mgmtClient + netSecClient := netsecgroup.NewClient(mgmtClient) + + // create and configure the RuleResponse: + name := d.Get("name").(string) + rule := netsecgroup.RuleRequest{ + // TODO(aznashwan): security checks here: + Name: name, + Type: netsecgroup.RuleType(d.Get("type").(string)), + Priority: d.Get("priority").(int), + Action: netsecgroup.RuleAction(d.Get("action").(string)), + SourceAddressPrefix: d.Get("source_address_prefix").(string), + SourcePortRange: d.Get("source_port_range").(string), + DestinationAddressPrefix: d.Get("destination_address_prefix").(string), + DestinationPortRange: d.Get("destination_port_range").(string), + Protocol: netsecgroup.RuleProtocol(d.Get("protocol").(string)), + } + + // send the create request to Azure: + log.Println("[INFO] Sending network security group rule creation request to Azure.") + reqID, err := netSecClient.SetNetworkSecurityGroupRule( + d.Get("security_group_name").(string), + rule, + ) + if err != nil { + return fmt.Errorf("Error sending network security group rule creation request to Azure: %s", err) + } + err = mgmtClient.WaitForOperation(reqID, nil) + if err != nil { + return fmt.Errorf("Error creating network security group rule on Azure: %s", err) + } + + d.SetId(name) + return nil +} + +// resourceAzureSecurityGroupRuleRead does all the necessary API calls to +// read the state of a network security group ruke off Azure. +func resourceAzureSecurityGroupRuleRead(d *schema.ResourceData, meta interface{}) error { + azureClient := meta.(*Client) + mgmtClient := azureClient.mgmtClient + netSecClient := netsecgroup.NewClient(mgmtClient) + + secGroupName := d.Get("security_group_name").(string) + + // get info on the network security group and check its rules for this one: + log.Println("[INFO] Sending network security group rule query to Azure.") + secgroup, err := netSecClient.GetNetworkSecurityGroup(secGroupName) + if err != nil { + if !management.IsResourceNotFoundError(err) { + return fmt.Errorf("Error issuing network security group rules query: %s", err) + } else { + // it meants that the network security group this rule belonged to has + // been deleted; so we must remove this resource from the schema: + d.SetId("") + return nil + } + } + + // find our security rule: + var found bool + name := d.Get("name").(string) + for _, rule := range secgroup.Rules { + if rule.Name == name { + found = true + log.Println("[DEBUG] Reading state of Azure network security group rule.") + + d.Set("type", rule.Type) + d.Set("priority", rule.Priority) + d.Set("action", rule.Action) + d.Set("source_address_prefix", rule.SourceAddressPrefix) + d.Set("source_port_range", rule.SourcePortRange) + d.Set("destination_address_prefix", rule.DestinationAddressPrefix) + d.Set("destination_port_range", rule.DestinationPortRange) + d.Set("protocol", rule.Protocol) + + break + } + } + + // check if the rule still exists, and is not, remove the resource: + if !found { + d.SetId("") + } + return nil +} + +// resourceAzureSecurityGroupRuleUpdate does all the necessary API calls to +// update the state of a network security group ruke off Azure. +func resourceAzureSecurityGroupRuleUpdate(d *schema.ResourceData, meta interface{}) error { + azureClient := meta.(*Client) + mgmtClient := azureClient.mgmtClient + netSecClient := netsecgroup.NewClient(mgmtClient) + + secGroupName := d.Get("security_group_name").(string) + + // get info on the network security group and check its rules for this one: + log.Println("[INFO] Sending network security group rule query for update to Azure.") + secgroup, err := netSecClient.GetNetworkSecurityGroup(secGroupName) + if err != nil { + if !management.IsResourceNotFoundError(err) { + return fmt.Errorf("Error issuing network security group rules query: %s", err) + } else { + // it meants that the network security group this rule belonged to has + // been deleted; so we must remove this resource from the schema: + d.SetId("") + return nil + } + } + + // try and find our security group rule: + var found bool + name := d.Get("name").(string) + for _, rule := range secgroup.Rules { + if rule.Name == name { + found = true + } + } + // check is the resource has not been deleted in the meantime: + if !found { + // if not; remove the resource: + d.SetId("") + return nil + } + + // else, start building up the rule request struct: + newRule := netsecgroup.RuleRequest{ + // TODO(azhnashwan): Parameter check here: + Name: d.Get("name").(string), + Type: netsecgroup.RuleType(d.Get("type").(string)), + Priority: d.Get("priority").(int), + Action: netsecgroup.RuleAction(d.Get("action").(string)), + SourceAddressPrefix: d.Get("source_address_prefix").(string), + SourcePortRange: d.Get("source_port_range").(string), + DestinationAddressPrefix: d.Get("destination_address_prefix").(string), + DestinationPortRange: d.Get("destination_port_range").(string), + Protocol: netsecgroup.RuleProtocol(d.Get("protocol").(string)), + } + + // send the create request to Azure: + log.Println("[INFO] Sending network security group rule update request to Azure.") + reqID, err := netSecClient.SetNetworkSecurityGroupRule( + secGroupName, + newRule, + ) + if err != nil { + return fmt.Errorf("Error sending network security group rule update request to Azure: %s", err) + } + err = mgmtClient.WaitForOperation(reqID, nil) + if err != nil { + return fmt.Errorf("Error updating network security group rule on Azure: %s", err) + } + + return nil +} + +// resourceAzureSecurityGroupRuleExists does all the necessary API calls to +// check for the existence of the network security group rule on Azure. +func resourceAzureSecurityGroupRuleExists(d *schema.ResourceData, meta interface{}) (bool, error) { + azureClient := meta.(*Client) + mgmtClient := azureClient.mgmtClient + netSecClient := netsecgroup.NewClient(mgmtClient) + + secGroupName := d.Get("security_group_name").(string) + + // get info on the network security group and search for our rule: + log.Println("[INFO] Sending network security group rule query for existence check to Azure.") + secgroup, err := netSecClient.GetNetworkSecurityGroup(secGroupName) + if err != nil { + if !management.IsResourceNotFoundError(err) { + return false, fmt.Errorf("Error issuing network security group rules query: %s", err) + } else { + // it meants that the network security group this rule belonged to has + // been deleted; so we must remove this resource from the schema: + d.SetId("") + return false, nil + } + } + + // try and find our security group rule: + name := d.Get("name").(string) + for _, rule := range secgroup.Rules { + if rule.Name == name { + return true, nil + } + } + + // if here; it means the resource has been deleted in the + // meantime and must be removed from the schema: + d.SetId("") + + return false, nil +} + +// resourceAzureSecurityGroupRuleDelete does all the necessary API calls to +// delete a network security group rule off Azure. +func resourceAzureSecurityGroupRuleDelete(d *schema.ResourceData, meta interface{}) error { + azureClient := meta.(*Client) + mgmtClient := azureClient.mgmtClient + netSecClient := netsecgroup.NewClient(mgmtClient) + + secGroupName := d.Get("security_group_name").(string) + + // get info on the network security group and search for our rule: + log.Println("[INFO] Sending network security group rule query for deletion to Azure.") + secgroup, err := netSecClient.GetNetworkSecurityGroup(secGroupName) + if err != nil { + if management.IsResourceNotFoundError(err) { + // it meants that the network security group this rule belonged to has + // been deleted; so we need do nothing more but stop tracking the resource: + d.SetId("") + return nil + } else { + return fmt.Errorf("Error issuing network security group rules query: %s", err) + } + } + + // check is the resource has not been deleted in the meantime: + name := d.Get("name").(string) + for _, rule := range secgroup.Rules { + if rule.Name == name { + // if not; we shall issue the delete: + reqID, err := netSecClient.DeleteNetworkSecurityGroupRule(secGroupName, name) + if err != nil { + return fmt.Errorf("Error sending network security group rule delete request to Azure: %s", err) + } + err = mgmtClient.WaitForOperation(reqID, nil) + if err != nil { + return fmt.Errorf("Error deleting network security group rule off Azure: %s", err) + } + } + } + + return nil +} diff --git a/builtin/providers/azure/resource_azure_security_group_rule_test.go b/builtin/providers/azure/resource_azure_security_group_rule_test.go new file mode 100644 index 0000000000..f16f29ced6 --- /dev/null +++ b/builtin/providers/azure/resource_azure_security_group_rule_test.go @@ -0,0 +1,109 @@ +package azure + +import ( + "fmt" + "testing" + + netsecgroup "github.com/Azure/azure-sdk-for-go/management/networksecuritygroup" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestAccAzureSecurityGroupRule(t *testing.T) { + name := "azure_security_group_rule.foo" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAzureSecurityGroupRuleDeleted, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccAzureSecurityGroupRule, + Check: resource.ComposeTestCheckFunc( + testAccCheckAzureSecurityGroupRuleExists(name), + resource.TestCheckResourceAttr(name, "name", "terraform-secgroup-rule"), + resource.TestCheckResourceAttr(name, "security_group_name", testAccSecurityGroupName), + resource.TestCheckResourceAttr(name, "type", "Inbound"), + resource.TestCheckResourceAttr(name, "action", "Deny"), + resource.TestCheckResourceAttr(name, "priority", "200"), + resource.TestCheckResourceAttr(name, "source_address_prefix", "100.0.0.0/32"), + resource.TestCheckResourceAttr(name, "source_port_range", "1000"), + resource.TestCheckResourceAttr(name, "destination_address_prefix", "10.0.0.0/32"), + resource.TestCheckResourceAttr(name, "protocol", "TCP"), + ), + }, + }, + }) +} + +func testAccCheckAzureSecurityGroupRuleExists(name string) resource.TestCheckFunc { + return func(s *terraform.State) error { + resource, ok := s.RootModule().Resources[name] + if !ok { + return fmt.Errorf("Azure security group rule not found: %s", name) + } + + if resource.Primary.ID == "" { + return fmt.Errorf("Azure network security group rule ID not set: %s", name) + } + + mgmtClient := testAccProvider.Meta().(*Client).mgmtClient + secGroupClient := netsecgroup.NewClient(mgmtClient) + + secGroup, err := secGroupClient.GetNetworkSecurityGroup(testAccSecurityGroupName) + if err != nil { + return fmt.Errorf("Failed getting network security group details: %s", err) + } + + for _, rule := range secGroup.Rules { + if rule.Name == resource.Primary.ID { + return nil + } + } + + return fmt.Errorf("Azure security group rule doesn't exist: %s", name) + } +} + +func testAccCheckAzureSecurityGroupRuleDeleted(s *terraform.State) error { + for _, resource := range s.RootModule().Resources { + if resource.Type != "azure_security_group_rule" { + continue + } + + if resource.Primary.ID == "" { + return fmt.Errorf("Azure network security group ID not set.") + } + + mgmtClient := testAccProvider.Meta().(*Client).mgmtClient + secGroupClient := netsecgroup.NewClient(mgmtClient) + + secGroup, err := secGroupClient.GetNetworkSecurityGroup(testAccSecurityGroupName) + if err != nil { + return fmt.Errorf("Failed getting network security group details: %s", err) + } + + for _, rule := range secGroup.Rules { + if rule.Name == resource.Primary.ID { + return fmt.Errorf("Azure network security group rule still exists!") + } + } + } + + return nil +} + +var testAccAzureSecurityGroupRule = testAccAzureSecurityGroupConfig + ` +resource "azure_security_group_rule" "foo" { + name = "terraform-secgroup-rule" + security_group_name = "${azure_security_group.foo.name}" + type = "Inbound" + action = "Deny" + priority = 200 + source_address_prefix = "100.0.0.0/32" + source_port_range = "1000" + destination_address_prefix = "10.0.0.0/32" + destination_port_range = "1000" + protocol = "TCP" +} +` diff --git a/builtin/providers/azure/resource_azure_security_group_test.go b/builtin/providers/azure/resource_azure_security_group_test.go index 31c73daf08..eb18d38e60 100644 --- a/builtin/providers/azure/resource_azure_security_group_test.go +++ b/builtin/providers/azure/resource_azure_security_group_test.go @@ -4,10 +4,10 @@ import ( "fmt" "testing" + "github.com/Azure/azure-sdk-for-go/management" + "github.com/Azure/azure-sdk-for-go/management/networksecuritygroup" "github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/terraform" - "github.com/svanharmelen/azure-sdk-for-go/management" - "github.com/svanharmelen/azure-sdk-for-go/management/networksecuritygroup" ) func TestAccAzureSecurityGroup_basic(t *testing.T) { @@ -19,72 +19,16 @@ func TestAccAzureSecurityGroup_basic(t *testing.T) { CheckDestroy: testAccCheckAzureSecurityGroupDestroy, Steps: []resource.TestStep{ resource.TestStep{ - Config: testAccAzureSecurityGroup_basic, + Config: testAccAzureSecurityGroupConfig, Check: resource.ComposeTestCheckFunc( testAccCheckAzureSecurityGroupExists( "azure_security_group.foo", &group), - testAccCheckAzureSecurityGroupBasicAttributes(&group), resource.TestCheckResourceAttr( "azure_security_group.foo", "name", "terraform-security-group"), resource.TestCheckResourceAttr( "azure_security_group.foo", "location", "West US"), resource.TestCheckResourceAttr( - "azure_security_group.foo", "rule.936204579.name", "RDP"), - resource.TestCheckResourceAttr( - "azure_security_group.foo", "rule.936204579.source_port", "*"), - resource.TestCheckResourceAttr( - "azure_security_group.foo", "rule.936204579.destination_port", "3389"), - ), - }, - }, - }) -} - -func TestAccAzureSecurityGroup_update(t *testing.T) { - var group networksecuritygroup.SecurityGroupResponse - - resource.Test(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, - CheckDestroy: testAccCheckAzureSecurityGroupDestroy, - Steps: []resource.TestStep{ - resource.TestStep{ - Config: testAccAzureSecurityGroup_basic, - Check: resource.ComposeTestCheckFunc( - testAccCheckAzureSecurityGroupExists( - "azure_security_group.foo", &group), - testAccCheckAzureSecurityGroupBasicAttributes(&group), - resource.TestCheckResourceAttr( - "azure_security_group.foo", "name", "terraform-security-group"), - resource.TestCheckResourceAttr( - "azure_security_group.foo", "location", "West US"), - resource.TestCheckResourceAttr( - "azure_security_group.foo", "rule.936204579.name", "RDP"), - resource.TestCheckResourceAttr( - "azure_security_group.foo", "rule.936204579.source_cidr", "*"), - resource.TestCheckResourceAttr( - "azure_security_group.foo", "rule.936204579.destination_port", "3389"), - ), - }, - - resource.TestStep{ - Config: testAccAzureSecurityGroup_update, - Check: resource.ComposeTestCheckFunc( - testAccCheckAzureSecurityGroupExists( - "azure_security_group.foo", &group), - testAccCheckAzureSecurityGroupUpdatedAttributes(&group), - resource.TestCheckResourceAttr( - "azure_security_group.foo", "rule.3322523298.name", "RDP"), - resource.TestCheckResourceAttr( - "azure_security_group.foo", "rule.3322523298.source_cidr", "192.168.0.0/24"), - resource.TestCheckResourceAttr( - "azure_security_group.foo", "rule.3322523298.destination_port", "3389"), - resource.TestCheckResourceAttr( - "azure_security_group.foo", "rule.3929353075.name", "WINRM"), - resource.TestCheckResourceAttr( - "azure_security_group.foo", "rule.3929353075.source_cidr", "192.168.0.0/24"), - resource.TestCheckResourceAttr( - "azure_security_group.foo", "rule.3929353075.destination_port", "5985"), + "azure_security_group.foo", "label", "terraform testing security group"), ), }, }, @@ -120,89 +64,6 @@ func testAccCheckAzureSecurityGroupExists( } } -func testAccCheckAzureSecurityGroupBasicAttributes( - group *networksecuritygroup.SecurityGroupResponse) resource.TestCheckFunc { - return func(s *terraform.State) error { - - if group.Name != "terraform-security-group" { - return fmt.Errorf("Bad name: %s", group.Name) - } - - for _, r := range group.Rules { - if !r.IsDefault { - if r.Name != "RDP" { - return fmt.Errorf("Bad rule name: %s", r.Name) - } - if r.Priority != 101 { - return fmt.Errorf("Bad rule priority: %d", r.Priority) - } - if r.SourceAddressPrefix != "*" { - return fmt.Errorf("Bad source CIDR: %s", r.SourceAddressPrefix) - } - if r.DestinationAddressPrefix != "*" { - return fmt.Errorf("Bad destination CIDR: %s", r.DestinationAddressPrefix) - } - if r.DestinationPortRange != "3389" { - return fmt.Errorf("Bad destination port: %s", r.DestinationPortRange) - } - } - } - - return nil - } -} - -func testAccCheckAzureSecurityGroupUpdatedAttributes( - group *networksecuritygroup.SecurityGroupResponse) resource.TestCheckFunc { - return func(s *terraform.State) error { - - if group.Name != "terraform-security-group" { - return fmt.Errorf("Bad name: %s", group.Name) - } - - foundRDP := false - foundWINRM := false - for _, r := range group.Rules { - if !r.IsDefault { - if r.Name == "RDP" { - if r.SourceAddressPrefix != "192.168.0.0/24" { - return fmt.Errorf("Bad source CIDR: %s", r.SourceAddressPrefix) - } - - foundRDP = true - } - - if r.Name == "WINRM" { - if r.Priority != 102 { - return fmt.Errorf("Bad rule priority: %d", r.Priority) - } - if r.SourceAddressPrefix != "192.168.0.0/24" { - return fmt.Errorf("Bad source CIDR: %s", r.SourceAddressPrefix) - } - if r.DestinationAddressPrefix != "*" { - return fmt.Errorf("Bad destination CIDR: %s", r.DestinationAddressPrefix) - } - if r.DestinationPortRange != "5985" { - return fmt.Errorf("Bad destination port: %s", r.DestinationPortRange) - } - - foundWINRM = true - } - } - } - - if !foundRDP { - return fmt.Errorf("RDP rule not found") - } - - if !foundWINRM { - return fmt.Errorf("WINRM rule not found") - } - - return nil - } -} - func testAccCheckAzureSecurityGroupDestroy(s *terraform.State) error { mc := testAccProvider.Meta().(*Client).mgmtClient @@ -228,44 +89,9 @@ func testAccCheckAzureSecurityGroupDestroy(s *terraform.State) error { return nil } -const testAccAzureSecurityGroup_basic = ` +var testAccAzureSecurityGroupConfig = fmt.Sprintf(` resource "azure_security_group" "foo" { - name = "terraform-security-group" + name = "%s" location = "West US" - - rule { - name = "RDP" - priority = 101 - source_cidr = "*" - source_port = "*" - destination_cidr = "*" - destination_port = "3389" - protocol = "TCP" - } -}` - -const testAccAzureSecurityGroup_update = ` -resource "azure_security_group" "foo" { - name = "terraform-security-group" - location = "West US" - - rule { - name = "RDP" - priority = 101 - source_cidr = "192.168.0.0/24" - source_port = "*" - destination_cidr = "*" - destination_port = "3389" - protocol = "TCP" - } - - rule { - name = "WINRM" - priority = 102 - source_cidr = "192.168.0.0/24" - source_port = "*" - destination_cidr = "*" - destination_port = "5985" - protocol = "TCP" - } -}` + label = "terraform testing security group" +}`, testAccSecurityGroupName) diff --git a/builtin/providers/azure/resource_azure_storage_blob.go b/builtin/providers/azure/resource_azure_storage_blob.go new file mode 100644 index 0000000000..613bbbb556 --- /dev/null +++ b/builtin/providers/azure/resource_azure_storage_blob.go @@ -0,0 +1,186 @@ +package azure + +import ( + "fmt" + "log" + + "github.com/hashicorp/terraform/helper/schema" +) + +// resourceAzureStorageBlob returns the *schema.Resource associated +// with a storage blob on Azure. +func resourceAzureStorageBlob() *schema.Resource { + return &schema.Resource{ + Create: resourceAzureStorageBlobCreate, + Read: resourceAzureStorageBlobRead, + Update: resourceAzureStorageBlobUpdate, + Exists: resourceAzureStorageBlobExists, + Delete: resourceAzureStorageBlobDelete, + + Schema: map[string]*schema.Schema{ + "name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: parameterDescriptions["name"], + }, + "type": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + DefaultFunc: func() (interface{}, error) { + return "BlockBlob", nil + }, + Description: parameterDescriptions["type"], + }, + "size": &schema.Schema{ + Type: schema.TypeInt, + Required: true, + ForceNew: true, + DefaultFunc: func() (interface{}, error) { + return int64(0), nil + }, + }, + "storage_container_name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: parameterDescriptions["storage_container_name"], + }, + "storage_service_name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: parameterDescriptions["storage_service_name"], + }, + "url": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + Description: parameterDescriptions["url"], + }, + }, + } +} + +// resourceAzureStorageBlobCreate does all the necessary API calls to +// create the storage blob on Azure. +func resourceAzureStorageBlobCreate(d *schema.ResourceData, meta interface{}) error { + mgmtClient := meta.(*Client).mgmtClient + storName := d.Get("storage_service_name").(string) + + blobClient, err := getStorageServiceBlobClient(mgmtClient, storName) + if err != nil { + return err + } + + log.Println("[INFO] Issuing create on Azure storage blob.") + name := d.Get("name").(string) + blobType := d.Get("type").(string) + cont := d.Get("storage_container_name").(string) + switch blobType { + case "BlockBlob": + err = blobClient.CreateBlockBlob(cont, name) + case "PageBlob": + size := int64(d.Get("size").(int)) + err = blobClient.PutPageBlob(cont, name, size) + default: + err = fmt.Errorf("Invalid blob type specified; see parameter desciptions for more info.") + } + if err != nil { + return fmt.Errorf("Error creating storage blob on Azure: %s", err) + } + + d.SetId(name) + return resourceAzureStorageBlobRead(d, meta) +} + +// resourceAzureStorageBlobRead does all the necessary API calls to +// read the status of the storage blob off Azure. +func resourceAzureStorageBlobRead(d *schema.ResourceData, meta interface{}) error { + // check for it's existence: + exists, err := resourceAzureStorageBlobExists(d, meta) + if err != nil { + return err + } + + // if it exists; read relevant information: + if exists { + mgmtClient := meta.(*Client).mgmtClient + storName := d.Get("storage_service_name").(string) + + blobClient, err := getStorageServiceBlobClient(mgmtClient, storName) + if err != nil { + return err + } + + name := d.Get("name").(string) + cont := d.Get("storage_container_name").(string) + url := blobClient.GetBlobURL(cont, name) + d.Set("url", url) + } + + // NOTE: no need to unset the ID here, as resourceAzureStorageBlobExists + // already should have done so if it were required. + return nil +} + +// resourceAzureStorageBlobUpdate does all the necessary API calls to +// update a blob on Azure. +func resourceAzureStorageBlobUpdate(d *schema.ResourceData, meta interface{}) error { + // NOTE: although empty as most paramters have ForceNew set; this is + // still required in case of changes to the storage_service_key + + // run the ExistsFunc beforehand to ensure the resource's existence nonetheless: + _, err := resourceAzureStorageBlobExists(d, meta) + return err +} + +// resourceAzureStorageBlobExists does all the necessary API calls to +// check for the existence of the blob on Azure. +func resourceAzureStorageBlobExists(d *schema.ResourceData, meta interface{}) (bool, error) { + mgmtClient := meta.(*Client).mgmtClient + storName := d.Get("storage_service_name").(string) + + blobClient, err := getStorageServiceBlobClient(mgmtClient, storName) + if err != nil { + return false, err + } + + log.Println("[INFO] Querying Azure for storage blob's existence.") + name := d.Get("name").(string) + cont := d.Get("storage_container_name").(string) + exists, err := blobClient.BlobExists(cont, name) + if err != nil { + return false, fmt.Errorf("Error whilst checking for Azure storage blob's existence: %s", err) + } + + // if not found; it means it was deleted in the meantime and + // we must remove it from the schema. + if !exists { + d.SetId("") + } + + return exists, nil +} + +// resourceAzureStorageBlobDelete does all the necessary API calls to +// delete the blob off Azure. +func resourceAzureStorageBlobDelete(d *schema.ResourceData, meta interface{}) error { + mgmtClient := meta.(*Client).mgmtClient + storName := d.Get("storage_service_name").(string) + + blobClient, err := getStorageServiceBlobClient(mgmtClient, storName) + if err != nil { + return err + } + + log.Println("[INFO] Issuing storage blob delete command off Azure.") + name := d.Get("name").(string) + cont := d.Get("storage_container_name").(string) + if _, err = blobClient.DeleteBlobIfExists(cont, name); err != nil { + return fmt.Errorf("Error whilst deleting storage blob: %s", err) + } + + d.SetId("") + return nil +} diff --git a/builtin/providers/azure/resource_azure_storage_blob_test.go b/builtin/providers/azure/resource_azure_storage_blob_test.go new file mode 100644 index 0000000000..9be701e213 --- /dev/null +++ b/builtin/providers/azure/resource_azure_storage_blob_test.go @@ -0,0 +1,139 @@ +package azure + +import ( + "fmt" + "testing" + "time" + + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestAccAzureStorageBlockBlob(t *testing.T) { + name := "azure_storage_blob.foo" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAzureStorageBlobDeleted, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccAzureStorageBlockBlobConfig, + Check: resource.ComposeTestCheckFunc( + testAccCheckAzureStorageBlobExists(name), + resource.TestCheckResourceAttr(name, "name", "tftesting-blob"), + resource.TestCheckResourceAttr(name, "type", "BlockBlob"), + resource.TestCheckResourceAttr(name, "storage_container_name", testAccStorageContainerName), + resource.TestCheckResourceAttr(name, "storage_service_name", testAccStorageServiceName), + ), + }, + }, + }) + + // because containers take a while to get deleted, sleep for a while: + time.Sleep(5 * time.Minute) +} + +func TestAccAzureStoragePageBlob(t *testing.T) { + name := "azure_storage_blob.foo" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAzureStorageBlobDeleted, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccAzureStoragePageBlobConfig, + Check: resource.ComposeTestCheckFunc( + testAccCheckAzureStorageBlobExists(name), + resource.TestCheckResourceAttr(name, "name", "tftesting-blob"), + resource.TestCheckResourceAttr(name, "type", "PageBlob"), + resource.TestCheckResourceAttr(name, "size", "512"), + resource.TestCheckResourceAttr(name, "storage_container_name", testAccStorageContainerName), + resource.TestCheckResourceAttr(name, "storage_service_name", testAccStorageServiceName), + ), + }, + }, + }) + + // because containers take a while to get deleted, sleep for a while: + time.Sleep(5 * time.Minute) +} + +func testAccCheckAzureStorageBlobExists(name string) resource.TestCheckFunc { + return func(s *terraform.State) error { + resource, ok := s.RootModule().Resources[name] + if !ok { + return fmt.Errorf("Azure Storage Container resource not found: %s", name) + } + + if resource.Primary.ID == "" { + return fmt.Errorf("Azure Storage Container ID not set: %s", name) + } + + mgmtClient := testAccProvider.Meta().(*Client).mgmtClient + blobClient, err := getStorageServiceBlobClient(mgmtClient, testAccStorageServiceName) + if err != nil { + return err + } + + exists, err := blobClient.BlobExists(testAccStorageContainerName, resource.Primary.ID) + if err != nil { + return err + } + if !exists { + return fmt.Errorf("Azure Storage Blob %s doesn't exist.", name) + } + + return nil + } +} + +func testAccCheckAzureStorageBlobDeleted(s *terraform.State) error { + for _, resource := range s.RootModule().Resources { + if resource.Type != "azure_storage_blob" { + continue + } + + mgmtClient := testAccProvider.Meta().(*Client).mgmtClient + blobClient, err := getStorageServiceBlobClient(mgmtClient, testAccStorageServiceName) + if err != nil { + return err + } + + exists, err := blobClient.BlobExists(testAccStorageContainerName, resource.Primary.ID) + if err != nil { + return err + } + if exists { + return fmt.Errorf("Azure Storage Blob still exists.") + } + } + + return nil + +} + +var testAccAzureStorageBlockBlobConfig = testAccAzureStorageContainerConfig + fmt.Sprintf(` +resource "azure_storage_blob" "foo" { + name = "tftesting-blob" + type = "BlockBlob" + # NOTE: A pre-existing Storage Service is used here so as to avoid + # the huge wait for creation of one. + storage_service_name = "%s" + storage_container_name = "%s" +} +`, testAccStorageServiceName, testAccStorageContainerName) + +var testAccAzureStoragePageBlobConfig = testAccAzureStorageContainerConfig + fmt.Sprintf(` +resource "azure_storage_blob" "foo" { + name = "tftesting-blob" + type = "PageBlob" + # NOTE: A pre-existing Storage Service is used here so as to avoid + # the huge wait for creation of one. + storage_service_name = "%s" + storage_container_name = "%s" + # NOTE: must be a multiple of 512: + size = 512 +} +`, testAccStorageServiceName, testAccStorageContainerName) diff --git a/builtin/providers/azure/resource_azure_storage_container.go b/builtin/providers/azure/resource_azure_storage_container.go new file mode 100644 index 0000000000..590189f48c --- /dev/null +++ b/builtin/providers/azure/resource_azure_storage_container.go @@ -0,0 +1,166 @@ +package azure + +import ( + "fmt" + "log" + + "github.com/Azure/azure-sdk-for-go/storage" + "github.com/hashicorp/terraform/helper/schema" +) + +// resourceAzureStorageContainer returns the *schema.Resource associated +// to a storage container on Azure. +func resourceAzureStorageContainer() *schema.Resource { + return &schema.Resource{ + Create: resourceAzureStorageContainerCreate, + Read: resourceAzureStorageContainerRead, + Exists: resourceAzureStorageContainerExists, + Delete: resourceAzureStorageContainerDelete, + + Schema: map[string]*schema.Schema{ + "name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: parameterDescriptions["name"], + }, + "storage_service_name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + DefaultFunc: func() (interface{}, error) { + return "", nil + }, + Description: parameterDescriptions["storage_service_name"], + }, + "container_access_type": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: parameterDescriptions["container_access_type"], + }, + "properties": &schema.Schema{ + Type: schema.TypeMap, + Computed: true, + Elem: schema.TypeString, + Description: parameterDescriptions["properties"], + }, + }, + } +} + +// resourceAzureStorageContainerCreate does all the necessary API calls to +// create the storage container on Azure. +func resourceAzureStorageContainerCreate(d *schema.ResourceData, meta interface{}) error { + mgmtClient := meta.(*Client).mgmtClient + storName := d.Get("storage_service_name").(string) + + blobClient, err := getStorageServiceBlobClient(mgmtClient, storName) + if err != nil { + return err + } + + log.Println("[INFO] Creating storage container on Azure.") + name := d.Get("name").(string) + accessType := storage.ContainerAccessType(d.Get("container_access_type").(string)) + err = blobClient.CreateContainer(name, accessType) + if err != nil { + return fmt.Errorf("Failed to create storage container on Azure: %s", err) + } + + d.SetId(name) + return resourceAzureStorageContainerRead(d, meta) +} + +// resourceAzureStorageContainerRead does all the necessary API calls to +// read the status of the storage container off Azure. +func resourceAzureStorageContainerRead(d *schema.ResourceData, meta interface{}) error { + mgmtClient := meta.(*Client).mgmtClient + storName := d.Get("storage_service_name").(string) + + blobClient, err := getStorageServiceBlobClient(mgmtClient, storName) + if err != nil { + return err + } + + log.Println("[INFO] Querying Azure for storage containers.") + name := d.Get("name").(string) + containers, err := blobClient.ListContainers(storage.ListContainersParameters{ + Prefix: name, + Timeout: 90, + }) + if err != nil { + return fmt.Errorf("Failed to query Azure for its storage containers: %s", err) + } + + // search for our storage container and update its stats: + var found bool + // loop just to make sure we got the right container: + for _, cont := range containers.Containers { + if cont.Name == name { + found = true + + props := make(map[string]interface{}) + props["last_modified"] = cont.Properties.LastModified + props["lease_status"] = cont.Properties.LeaseStatus + props["lease_state"] = cont.Properties.LeaseState + props["lease_duration"] = cont.Properties.LeaseDuration + + d.Set("properties", props) + } + } + + // if not found; it means the resource has been deleted + // in the meantime; so we must untrack it: + if !found { + d.SetId("") + } + + return nil +} + +// resourceAzureStorageContainerExists does all the necessary API calls to +// check if the storage container already exists on Azure. +func resourceAzureStorageContainerExists(d *schema.ResourceData, meta interface{}) (bool, error) { + mgmtClient := meta.(*Client).mgmtClient + storName := d.Get("storage_service_name").(string) + + blobClient, err := getStorageServiceBlobClient(mgmtClient, storName) + if err != nil { + return false, err + } + + log.Println("[INFO] Checking existence of storage container on Azure.") + name := d.Get("name").(string) + exists, err := blobClient.ContainerExists(name) + if err != nil { + return false, fmt.Errorf("Failed to query for Azure storage container existence: %s", err) + } + + // if it does not exist; untrack the resource: + if !exists { + d.SetId("") + } + return exists, nil +} + +// resourceAzureStorageContainerDelete does all the necessary API calls to +// delete a storage container off Azure. +func resourceAzureStorageContainerDelete(d *schema.ResourceData, meta interface{}) error { + mgmtClient := meta.(*Client).mgmtClient + storName := d.Get("storage_service_name").(string) + + blobClient, err := getStorageServiceBlobClient(mgmtClient, storName) + if err != nil { + return err + } + + log.Println("[INFO] Issuing Azure storage container deletion call.") + name := d.Get("name").(string) + if _, err := blobClient.DeleteContainerIfExists(name); err != nil { + return fmt.Errorf("Failed deleting storage container off Azure: %s", err) + } + + d.SetId("") + return nil +} diff --git a/builtin/providers/azure/resource_azure_storage_container_test.go b/builtin/providers/azure/resource_azure_storage_container_test.go new file mode 100644 index 0000000000..65de189281 --- /dev/null +++ b/builtin/providers/azure/resource_azure_storage_container_test.go @@ -0,0 +1,96 @@ +package azure + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestAccAzureStorageContainer(t *testing.T) { + name := "azure_storage_container.foo" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAzureStorageContainerDestroyed, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccAzureStorageContainerConfig, + Check: resource.ComposeTestCheckFunc( + testAccCheckAzureStorageContainerExists(name), + resource.TestCheckResourceAttr(name, "name", testAccStorageContainerName), + resource.TestCheckResourceAttr(name, "storage_service_name", testAccStorageServiceName), + resource.TestCheckResourceAttr(name, "container_access_type", "blob"), + ), + }, + }, + }) + + // because containers take a while to get deleted, sleep for one minute: + time.Sleep(3 * time.Minute) +} + +func testAccCheckAzureStorageContainerExists(name string) resource.TestCheckFunc { + return func(s *terraform.State) error { + resource, ok := s.RootModule().Resources[name] + if !ok { + return fmt.Errorf("Azure Storage Container resource not found: %s", name) + } + + if resource.Primary.ID == "" { + return fmt.Errorf("Azure Storage Container ID not set: %s", name) + } + + mgmtClient := testAccProvider.Meta().(*Client).mgmtClient + blobClient, err := getStorageServiceBlobClient(mgmtClient, testAccStorageServiceName) + if err != nil { + return err + } + + exists, err := blobClient.ContainerExists(resource.Primary.ID) + if err != nil { + return err + } + if !exists { + return fmt.Errorf("Azure Storage Container %s doesn't exist.", name) + } + + return nil + } +} + +func testAccCheckAzureStorageContainerDestroyed(s *terraform.State) error { + for _, resource := range s.RootModule().Resources { + if resource.Type != "azure_storage_container" { + continue + } + + mgmtClient := testAccProvider.Meta().(*Client).mgmtClient + blobClient, err := getStorageServiceBlobClient(mgmtClient, testAccStorageServiceName) + if err != nil { + return err + } + + exists, err := blobClient.ContainerExists(resource.Primary.ID) + if err != nil { + return err + } + if exists { + return fmt.Errorf("Azure Storage Container still exists.") + } + } + + return nil +} + +var testAccAzureStorageContainerConfig = fmt.Sprintf(` +resource "azure_storage_container" "foo" { + name = "%s" + container_access_type = "blob" + # NOTE: A pre-existing Storage Service is used here so as to avoid + # the huge wait for creation of one. + storage_service_name = "%s" +} +`, testAccStorageContainerName, testAccStorageServiceName) diff --git a/builtin/providers/azure/resource_azure_storage_service.go b/builtin/providers/azure/resource_azure_storage_service.go new file mode 100644 index 0000000000..4292cc47e1 --- /dev/null +++ b/builtin/providers/azure/resource_azure_storage_service.go @@ -0,0 +1,227 @@ +package azure + +import ( + "encoding/base64" + "fmt" + "log" + + "github.com/Azure/azure-sdk-for-go/management" + "github.com/Azure/azure-sdk-for-go/management/storageservice" + "github.com/hashicorp/terraform/helper/schema" +) + +// resourceAzureStorageService returns the *schema.Resource associated +// to an Azure hosted service. +func resourceAzureStorageService() *schema.Resource { + return &schema.Resource{ + Create: resourceAzureStorageServiceCreate, + Read: resourceAzureStorageServiceRead, + Exists: resourceAzureStorageServiceExists, + Delete: resourceAzureStorageServiceDelete, + + Schema: map[string]*schema.Schema{ + // General attributes: + "name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + // TODO(aznashwan): constrain name in description + Description: parameterDescriptions["name"], + }, + "location": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: parameterDescriptions["location"], + }, + "label": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Default: "Made by Terraform.", + Description: parameterDescriptions["label"], + }, + "description": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Description: parameterDescriptions["description"], + }, + // Functional attributes: + "account_type": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: parameterDescriptions["account_type"], + }, + "affinity_group": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Description: parameterDescriptions["affinity_group"], + }, + "properties": &schema.Schema{ + Type: schema.TypeMap, + Optional: true, + ForceNew: true, + Elem: schema.TypeString, + }, + // Computed attributes: + "url": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + "primary_key": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + "secondary_key": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + }, + } +} + +// resourceAzureStorageServiceCreate does all the necessary API calls to +// create a new Azure storage service. +func resourceAzureStorageServiceCreate(d *schema.ResourceData, meta interface{}) error { + azureClient := meta.(*Client) + mgmtClient := azureClient.mgmtClient + storageServiceClient := storageservice.NewClient(mgmtClient) + + // get all the values: + log.Println("[INFO] Creating Azure Storage Service creation parameters.") + name := d.Get("name").(string) + location := d.Get("location").(string) + accountType := storageservice.AccountType(d.Get("account_type").(string)) + affinityGroup := d.Get("affinity_group").(string) + description := d.Get("description").(string) + label := base64.StdEncoding.EncodeToString([]byte(d.Get("label").(string))) + var props []storageservice.ExtendedProperty + if given := d.Get("properties").(map[string]interface{}); len(given) > 0 { + props = []storageservice.ExtendedProperty{} + for k, v := range given { + props = append(props, storageservice.ExtendedProperty{ + Name: k, + Value: v.(string), + }) + } + } + + // create parameters and send request: + log.Println("[INFO] Sending Storage Service creation request to Azure.") + reqID, err := storageServiceClient.CreateStorageService( + storageservice.StorageAccountCreateParameters{ + ServiceName: name, + Location: location, + Description: description, + Label: label, + AffinityGroup: affinityGroup, + AccountType: accountType, + ExtendedProperties: storageservice.ExtendedPropertyList{ + ExtendedProperty: props, + }, + }) + if err != nil { + return fmt.Errorf("Failed to create Azure storage service %s: %s", name, err) + } + err = mgmtClient.WaitForOperation(reqID, nil) + if err != nil { + return fmt.Errorf("Failed creating storage service %s: %s", name, err) + } + + d.SetId(name) + return resourceAzureStorageServiceRead(d, meta) +} + +// resourceAzureStorageServiceRead does all the necessary API calls to +// read the state of the storage service off Azure. +func resourceAzureStorageServiceRead(d *schema.ResourceData, meta interface{}) error { + azureClient := meta.(*Client) + mgmtClient := azureClient.mgmtClient + storageServiceClient := storageservice.NewClient(mgmtClient) + + // get our storage service: + log.Println("[INFO] Sending query about storage service to Azure.") + name := d.Get("name").(string) + storsvc, err := storageServiceClient.GetStorageService(name) + if err != nil { + if !management.IsResourceNotFoundError(err) { + return fmt.Errorf("Failed to query about Azure about storage service: %s", err) + } else { + // it means that the resource has been deleted from Azure + // in the meantime and we must remove its associated Resource. + d.SetId("") + return nil + + } + } + + // read values: + d.Set("url", storsvc.URL) + log.Println("[INFO] Querying keys of Azure storage service.") + keys, err := storageServiceClient.GetStorageServiceKeys(name) + if err != nil { + return fmt.Errorf("Failed querying keys for Azure storage service: %s", err) + } + d.Set("primary_key", keys.PrimaryKey) + d.Set("secondary_key", keys.SecondaryKey) + + return nil +} + +// resourceAzureStorageServiceExists does all the necessary API calls to +// check if the storage service exists on Azure. +func resourceAzureStorageServiceExists(d *schema.ResourceData, meta interface{}) (bool, error) { + azureClient, ok := meta.(*Client) + if !ok { + return false, fmt.Errorf("Failed to convert to *Client, got: %T", meta) + } + mgmtClient := azureClient.mgmtClient + storageServiceClient := storageservice.NewClient(mgmtClient) + + // get our storage service: + log.Println("[INFO] Sending query about storage service to Azure.") + name := d.Get("name").(string) + _, err := storageServiceClient.GetStorageService(name) + if err != nil { + if !management.IsResourceNotFoundError(err) { + return false, fmt.Errorf("Failed to query about Azure about storage service: %s", err) + } else { + // it means that the resource has been deleted from Azure + // in the meantime and we must remove its associated Resource. + d.SetId("") + return false, nil + + } + } + + return true, nil +} + +// resourceAzureStorageServiceDelete does all the necessary API calls to +// delete the storage service off Azure. +func resourceAzureStorageServiceDelete(d *schema.ResourceData, meta interface{}) error { + azureClient, ok := meta.(*Client) + if !ok { + return fmt.Errorf("Failed to convert to *Client, got: %T", meta) + } + mgmtClient := azureClient.mgmtClient + storageClient := storageservice.NewClient(mgmtClient) + + // issue the deletion: + name := d.Get("name").(string) + log.Println("[INFO] Issuing delete of storage service off Azure.") + reqID, err := storageClient.DeleteStorageService(name) + if err != nil { + return fmt.Errorf("Error whilst issuing deletion of storage service off Azure: %s", err) + } + err = mgmtClient.WaitForOperation(reqID, nil) + if err != nil { + return fmt.Errorf("Error whilst deleting storage service off Azure: %s", err) + } + + d.SetId("") + return nil +} diff --git a/builtin/providers/azure/resource_azure_storage_service_helpers.go b/builtin/providers/azure/resource_azure_storage_service_helpers.go new file mode 100644 index 0000000000..26cce502f0 --- /dev/null +++ b/builtin/providers/azure/resource_azure_storage_service_helpers.go @@ -0,0 +1,31 @@ +package azure + +import ( + "fmt" + "log" + + "github.com/Azure/azure-sdk-for-go/management" + "github.com/Azure/azure-sdk-for-go/management/storageservice" + "github.com/Azure/azure-sdk-for-go/storage" +) + +// getStorageServiceBlobClient is a helper function which returns the +// storage.BlobStorageClient associated to the given storage account name. +func getStorageServiceBlobClient(mgmtClient management.Client, serviceName string) (storage.BlobStorageClient, error) { + log.Println("[INFO] Begun generating Azure Storage Service Blob client.") + var blobClient storage.BlobStorageClient + + storageServiceClient := storageservice.NewClient(mgmtClient) + + keys, err := storageServiceClient.GetStorageServiceKeys(serviceName) + if err != nil { + return blobClient, fmt.Errorf("Error reading Storage Service %s's keys from Azure: %s", serviceName, err) + } + + storageClient, err := storage.NewBasicClient(serviceName, keys.PrimaryKey) + if err != nil { + return blobClient, fmt.Errorf("Error creating Storage Service Client for %s: %s", serviceName, err) + } + + return storageClient.GetBlobService(), nil +} diff --git a/builtin/providers/azure/resource_azure_storage_service_test.go b/builtin/providers/azure/resource_azure_storage_service_test.go new file mode 100644 index 0000000000..4f1a54e2b4 --- /dev/null +++ b/builtin/providers/azure/resource_azure_storage_service_test.go @@ -0,0 +1,79 @@ +package azure + +import ( + "fmt" + "testing" + + "github.com/Azure/azure-sdk-for-go/management/storageservice" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestAccAzureStorageService(t *testing.T) { + name := "azure_storage_service.foo" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccAzureStorageServiceDestroyed, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccAzureStorageServiceConfig, + Check: resource.ComposeTestCheckFunc( + testAccAzureStorageServiceExists(name), + resource.TestCheckResourceAttr(name, "name", "tftesting"), + resource.TestCheckResourceAttr(name, "location", "North Europe"), + resource.TestCheckResourceAttr(name, "description", "very descriptive"), + resource.TestCheckResourceAttr(name, "account_type", "Standard_LRS"), + ), + }, + }, + }) +} + +func testAccAzureStorageServiceExists(name string) resource.TestCheckFunc { + return func(s *terraform.State) error { + resource, ok := s.RootModule().Resources[name] + if !ok { + return fmt.Errorf("Azure Storage Service Resource not found: %s", name) + } + + if resource.Primary.ID == "" { + return fmt.Errorf("Azure Storage Service ID not set.") + } + + mgmtClient := testAccProvider.Meta().(*Client).mgmtClient + _, err := storageservice.NewClient(mgmtClient).GetStorageService(resource.Primary.ID) + + return err + } +} + +func testAccAzureStorageServiceDestroyed(s *terraform.State) error { + mgmgClient := testAccProvider.Meta().(*Client).mgmtClient + + for _, resource := range s.RootModule().Resources { + if resource.Type != "azure_storage_service" { + continue + } + + if resource.Primary.ID == "" { + return fmt.Errorf("Azure Storage Service ID not set.") + } + + _, err := storageservice.NewClient(mgmgClient).GetStorageService(resource.Primary.ID) + return testAccResourceDestroyedErrorFilter("Storage Service", err) + } + + return nil +} + +var testAccAzureStorageServiceConfig = ` +resource "azure_storage_service" "foo" { + # NOTE: storage service names constrained to lowercase letters only. + name = "tftesting" + location = "West US" + description = "very descriptive" + account_type = "Standard_LRS" +} +` diff --git a/builtin/providers/azure/resource_azure_virtual_network.go b/builtin/providers/azure/resource_azure_virtual_network.go index be66c1a42f..c5c250c555 100644 --- a/builtin/providers/azure/resource_azure_virtual_network.go +++ b/builtin/providers/azure/resource_azure_virtual_network.go @@ -5,12 +5,11 @@ import ( "log" "strings" + "github.com/Azure/azure-sdk-for-go/management" + "github.com/Azure/azure-sdk-for-go/management/networksecuritygroup" + "github.com/Azure/azure-sdk-for-go/management/virtualnetwork" "github.com/hashicorp/terraform/helper/hashcode" "github.com/hashicorp/terraform/helper/schema" - "github.com/mitchellh/mapstructure" - "github.com/svanharmelen/azure-sdk-for-go/management" - "github.com/svanharmelen/azure-sdk-for-go/management/networksecuritygroup" - "github.com/svanharmelen/azure-sdk-for-go/management/virtualnetwork" ) const ( @@ -37,6 +36,14 @@ func resourceAzureVirtualNetwork() *schema.Resource { Elem: &schema.Schema{Type: schema.TypeString}, }, + "dns_servers_names": &schema.Schema{ + Type: schema.TypeList, + Optional: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + "subnet": &schema.Schema{ Type: schema.TypeSet, Required: true, @@ -94,11 +101,7 @@ func resourceAzureVirtualNetworkCreate(d *schema.ResourceData, meta interface{}) } } - network, err := createVirtualNetwork(d) - if err != nil { - return err - } - + network := createVirtualNetwork(d) nc.Configuration.VirtualNetworkSites = append(nc.Configuration.VirtualNetworkSites, network) req, err := virtualnetwork.NewClient(mc).SetVirtualNetworkConfiguration(nc) @@ -187,11 +190,7 @@ func resourceAzureVirtualNetworkUpdate(d *schema.ResourceData, meta interface{}) found := false for i, n := range nc.Configuration.VirtualNetworkSites { if n.Name == d.Id() { - network, err := createVirtualNetwork(d) - if err != nil { - return err - } - + network := createVirtualNetwork(d) nc.Configuration.VirtualNetworkSites[i] = network found = true @@ -263,15 +262,19 @@ func resourceAzureSubnetHash(v interface{}) int { return hashcode.String(subnet) } -func createVirtualNetwork(d *schema.ResourceData) (virtualnetwork.VirtualNetworkSite, error) { - var addressPrefix []string - err := mapstructure.WeakDecode(d.Get("address_space"), &addressPrefix) - if err != nil { - return virtualnetwork.VirtualNetworkSite{}, fmt.Errorf("Error decoding address_space: %s", err) +func createVirtualNetwork(d *schema.ResourceData) virtualnetwork.VirtualNetworkSite { + // fetch address spaces: + var prefixes []string + for _, prefix := range d.Get("address_space").([]interface{}) { + prefixes = append(prefixes, prefix.(string)) } - addressSpace := virtualnetwork.AddressSpace{ - AddressPrefix: addressPrefix, + // fetch DNS references: + var dnsRefs []virtualnetwork.DNSServerRef + for _, dns := range d.Get("dns_servers_names").([]interface{}) { + dnsRefs = append(dnsRefs, virtualnetwork.DNSServerRef{ + Name: dns.(string), + }) } // Add all subnets that are configured @@ -287,11 +290,14 @@ func createVirtualNetwork(d *schema.ResourceData) (virtualnetwork.VirtualNetwork } return virtualnetwork.VirtualNetworkSite{ - Name: d.Get("name").(string), - Location: d.Get("location").(string), - AddressSpace: addressSpace, - Subnets: subnets, - }, nil + Name: d.Get("name").(string), + Location: d.Get("location").(string), + AddressSpace: virtualnetwork.AddressSpace{ + AddressPrefix: prefixes, + }, + DNSServersRef: dnsRefs, + Subnets: subnets, + } } func associateSecurityGroups(d *schema.ResourceData, meta interface{}) error { diff --git a/builtin/providers/azure/resource_azure_virtual_network_test.go b/builtin/providers/azure/resource_azure_virtual_network_test.go index cc0817f03c..70327cad05 100644 --- a/builtin/providers/azure/resource_azure_virtual_network_test.go +++ b/builtin/providers/azure/resource_azure_virtual_network_test.go @@ -4,9 +4,9 @@ import ( "fmt" "testing" + "github.com/Azure/azure-sdk-for-go/management/virtualnetwork" "github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/terraform" - "github.com/svanharmelen/azure-sdk-for-go/management/virtualnetwork" ) func TestAccAzureVirtualNetwork_basic(t *testing.T) { @@ -214,16 +214,19 @@ const testAccAzureVirtualNetwork_advanced = ` resource "azure_security_group" "foo" { name = "terraform-security-group1" location = "West US" +} - rule { - name = "RDP" - priority = 101 - source_cidr = "*" - source_port = "*" - destination_cidr = "*" - destination_port = "3389" - protocol = "TCP" - } +resource "azure_security_group_rule" "foo" { + name = "terraform-secgroup-rule" + security_group_name = "${azure_security_group.foo.name}" + type = "Inbound" + action = "Deny" + priority = 200 + source_address_prefix = "100.0.0.0/32" + source_port_range = "1000" + destination_address_prefix = "10.0.0.0/32" + destination_port_range = "1000" + protocol = "TCP" } resource "azure_virtual_network" "foo" { @@ -242,31 +245,24 @@ const testAccAzureVirtualNetwork_update = ` resource "azure_security_group" "foo" { name = "terraform-security-group1" location = "West US" +} - rule { - name = "RDP" - priority = 101 - source_cidr = "*" - source_port = "*" - destination_cidr = "*" - destination_port = "3389" - protocol = "TCP" - } +resource "azure_security_group_rule" "foo" { + name = "terraform-secgroup-rule" + security_group_name = "${azure_security_group.foo.name}" + type = "Inbound" + action = "Deny" + priority = 200 + source_address_prefix = "100.0.0.0/32" + source_port_range = "1000" + destination_address_prefix = "10.0.0.0/32" + destination_port_range = "1000" + protocol = "TCP" } resource "azure_security_group" "bar" { name = "terraform-security-group2" location = "West US" - - rule { - name = "SSH" - priority = 101 - source_cidr = "*" - source_port = "*" - destination_cidr = "*" - destination_port = "22" - protocol = "TCP" - } } resource "azure_virtual_network" "foo" { diff --git a/builtin/providers/azure/utils_test.go b/builtin/providers/azure/utils_test.go new file mode 100644 index 0000000000..3a32122633 --- /dev/null +++ b/builtin/providers/azure/utils_test.go @@ -0,0 +1,20 @@ +package azure + +import ( + "fmt" + + "github.com/Azure/azure-sdk-for-go/management" +) + +// testAccResourceDestroyedErrorFilter tests whether the given error is an azure ResourceNotFound +// error and properly annotates it if otherwise: +func testAccResourceDestroyedErrorFilter(resource string, err error) error { + switch { + case err == nil: + return fmt.Errorf("Azure %s still exists.", resource) + case err != nil && management.IsResourceNotFoundError(err): + return nil + default: + return err + } +}