diff --git a/builtin/bins/provider-azure/main.go b/builtin/bins/provider-azure/main.go new file mode 100644 index 000000000..45af21656 --- /dev/null +++ b/builtin/bins/provider-azure/main.go @@ -0,0 +1,12 @@ +package main + +import ( + "github.com/hashicorp/terraform/builtin/providers/azure" + "github.com/hashicorp/terraform/plugin" +) + +func main() { + plugin.Serve(&plugin.ServeOpts{ + ProviderFunc: azure.Provider, + }) +} diff --git a/builtin/bins/provider-azure/main_test.go b/builtin/bins/provider-azure/main_test.go new file mode 100644 index 000000000..06ab7d0f9 --- /dev/null +++ b/builtin/bins/provider-azure/main_test.go @@ -0,0 +1 @@ +package main diff --git a/builtin/providers/azure/config.go b/builtin/providers/azure/config.go new file mode 100644 index 000000000..4f093d591 --- /dev/null +++ b/builtin/providers/azure/config.go @@ -0,0 +1,30 @@ +package azure + +import ( + "fmt" + "log" + "os" + + azure "github.com/MSOpenTech/azure-sdk-for-go" +) + +type Config struct { + PublishSettingsFile string +} + +func (c *Config) loadAndValidate() error { + if _, err := os.Stat(c.PublishSettingsFile); os.IsNotExist(err) { + return fmt.Errorf( + "Error loading Azure Publish Settings file '%s': %s", + c.PublishSettingsFile, + err) + } + + log.Printf("[INFO] Importing Azure Publish Settings file...") + err := azure.ImportPublishSettingsFile(c.PublishSettingsFile) + if err != nil { + return err + } + + return nil +} diff --git a/builtin/providers/azure/provider.go b/builtin/providers/azure/provider.go new file mode 100644 index 000000000..199491e37 --- /dev/null +++ b/builtin/providers/azure/provider.go @@ -0,0 +1,48 @@ +package azure + +import ( + "os" + + "github.com/hashicorp/terraform/helper/schema" + "github.com/hashicorp/terraform/terraform" +) + +func Provider() terraform.ResourceProvider { + return &schema.Provider{ + Schema: map[string]*schema.Schema{ + "publish_settings_file": &schema.Schema{ + Type: schema.TypeString, + Required: true, + DefaultFunc: envDefaultFunc("AZURE_PUBLISH_SETTINGS_FILE"), + }, + }, + + ResourcesMap: map[string]*schema.Resource{ + "azure_virtual_machine": resourceVirtualMachine(), + }, + + ConfigureFunc: providerConfigure, + } +} + +func envDefaultFunc(k string) schema.SchemaDefaultFunc { + return func() (interface{}, error) { + if v := os.Getenv(k); v != "" { + return v, nil + } + + return nil, nil + } +} + +func providerConfigure(d *schema.ResourceData) (interface{}, error) { + config := Config{ + PublishSettingsFile: d.Get("publish_settings_file").(string), + } + + if err := config.loadAndValidate(); err != nil { + return nil, err + } + + return &config, nil +} diff --git a/builtin/providers/azure/provider_test.go b/builtin/providers/azure/provider_test.go new file mode 100644 index 000000000..4a40c5301 --- /dev/null +++ b/builtin/providers/azure/provider_test.go @@ -0,0 +1,35 @@ +package azure + +import ( + "os" + "testing" + + "github.com/hashicorp/terraform/helper/schema" + "github.com/hashicorp/terraform/terraform" +) + +var testAccProviders map[string]terraform.ResourceProvider +var testAccProvider *schema.Provider + +func init() { + testAccProvider = Provider().(*schema.Provider) + testAccProviders = map[string]terraform.ResourceProvider{ + "azure": testAccProvider, + } +} + +func TestProvider(t *testing.T) { + if err := Provider().(*schema.Provider).InternalValidate(); err != nil { + t.Fatalf("err: %s", err) + } +} + +func TestProvider_impl(t *testing.T) { + var _ terraform.ResourceProvider = Provider() +} + +func testAccPreCheck(t *testing.T) { + if v := os.Getenv("AZURE_PUBLISH_SETTINGS_FILE"); v == "" { + t.Fatal("AZURE_PUBLISH_SETTINGS_FILE must be set for acceptance tests") + } +} diff --git a/builtin/providers/azure/resource_virtual_machine.go b/builtin/providers/azure/resource_virtual_machine.go new file mode 100644 index 000000000..88dd9f9fb --- /dev/null +++ b/builtin/providers/azure/resource_virtual_machine.go @@ -0,0 +1,241 @@ +package azure + +import ( + "bytes" + "fmt" + "log" + + "github.com/hashicorp/terraform/helper/hashcode" + "github.com/hashicorp/terraform/helper/schema" + "github.com/MSOpenTech/azure-sdk-for-go/clients/vmClient" +) + +func resourceVirtualMachine() *schema.Resource { + return &schema.Resource{ + Create: resourceVirtualMachineCreate, + Read: resourceVirtualMachineRead, + Delete: resourceVirtualMachineDelete, + + Schema: map[string]*schema.Schema{ + "name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "location": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "image": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "size": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "username": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "password": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Default: "", + ForceNew: true, + }, + + "ssh_public_key_file": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Default: "", + ForceNew: true, + }, + + "ssh_port": &schema.Schema{ + Type: schema.TypeInt, + Optional: true, + Default: 22, + ForceNew: true, + }, + + "endpoint": &schema.Schema{ + Type: schema.TypeSet, + Optional: true, + Computed: true, + ForceNew: true, // This can be updatable once we support updates on the resource + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + + "protocol": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + + "port": &schema.Schema{ + Type: schema.TypeInt, + Required: true, + }, + + "local_port": &schema.Schema{ + Type: schema.TypeInt, + Required: true, + }, + }, + }, + Set: resourceVirtualMachineEndpointHash, + }, + + "url": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + + "ip_address": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + + "vip_address": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + }, + } +} + +func resourceVirtualMachineCreate(d *schema.ResourceData, meta interface{}) error { + log.Printf("[DEBUG] Creating Azure Virtual Machine Configuration...") + vmConfig, err := vmClient.CreateAzureVMConfiguration( + d.Get("name").(string), + d.Get("size").(string), + d.Get("image").(string), + d.Get("location").(string)) + if err != nil { + return fmt.Errorf("Error creating Azure virtual machine configuration: %s", err) + } + + // Only Linux VMs are supported. If we want to support other VM types, we need to + // grab the image details and based on the OS add the corresponding configuration. + log.Printf("[DEBUG] Adding Azure Linux Provisioning Configuration...") + vmConfig, err = vmClient.AddAzureLinuxProvisioningConfig( + vmConfig, + d.Get("username").(string), + d.Get("password").(string), + d.Get("ssh_public_key_file").(string), + d.Get("ssh_port").(int)) + if err != nil { + return fmt.Errorf("Error adding Azure linux provisioning configuration: %s", err) + } + + if v := d.Get("endpoint").(*schema.Set); v.Len() > 0 { + log.Printf("[DEBUG] Adding Endpoints to the Azure Virtual Machine...") + endpoints := make([]vmClient.InputEndpoint, v.Len()) + for i, v := range v.List() { + m := v.(map[string]interface{}) + endpoint := vmClient.InputEndpoint{} + endpoint.Name = m["name"].(string) + endpoint.Protocol = m["protocol"].(string) + endpoint.Port = m["port"].(int) + endpoint.LocalPort = m["local_port"].(int) + endpoints[i] = endpoint + } + + configSets := vmConfig.ConfigurationSets.ConfigurationSet + if len(configSets) == 0 { + return fmt.Errorf("Azure virtual machine does not have configuration sets") + } + for i := 0; i < len(configSets); i++ { + if configSets[i].ConfigurationSetType != "NetworkConfiguration" { + continue + } + configSets[i].InputEndpoints.InputEndpoint = + append(configSets[i].InputEndpoints.InputEndpoint, endpoints...) + } + } + + log.Printf("[DEBUG] Creating Azure Virtual Machine...") + err = vmClient.CreateAzureVM( + vmConfig, + d.Get("name").(string), + d.Get("location").(string)) + if err != nil { + return fmt.Errorf("Error creating Azure virtual machine: %s", err) + } + + d.SetId(d.Get("name").(string)) + + return resourceVirtualMachineRead(d, meta) +} + +func resourceVirtualMachineRead(d *schema.ResourceData, meta interface{}) error { + log.Printf("[DEBUG] Getting Azure Virtual Machine Deployment: %s", d.Id()) + VMDeployment, err := vmClient.GetVMDeployment(d.Id(), d.Id()) + if err != nil { + return fmt.Errorf("Error getting Azure virtual machine deployment: %s", err) + } + + d.Set("url", VMDeployment.Url) + + roleInstances := VMDeployment.RoleInstanceList.RoleInstance + if len(roleInstances) == 0 { + return fmt.Errorf("Virtual Machine does not have IP addresses") + } + ipAddress := roleInstances[0].IpAddress + d.Set("ip_address", ipAddress) + + vips := VMDeployment.VirtualIPs.VirtualIP + if len(vips) == 0 { + return fmt.Errorf("Virtual Machine does not have VIP addresses") + } + vip := vips[0].Address + d.Set("vip_address", vip) + + d.SetConnInfo(map[string]string{ + "type": "ssh", + "host": vip, + "user": d.Get("username").(string), + }) + + return nil +} + +func resourceVirtualMachineDelete(d *schema.ResourceData, meta interface{}) error { + log.Printf("[DEBUG] Deleting Azure Virtual Machine Deployment: %s", d.Id()) + if err := vmClient.DeleteVMDeployment(d.Id(), d.Id()); err != nil { + return fmt.Errorf("Error deleting Azure virtual machine deployment: %s", err) + } + + log.Printf("[DEBUG] Deleting Azure Hosted Service: %s", d.Id()) + if err := vmClient.DeleteHostedService(d.Id()); err != nil { + return fmt.Errorf("Error deleting Azure hosted service: %s", err) + } + + d.SetId("") + + return nil +} + +func resourceVirtualMachineEndpointHash(v interface{}) int { + var buf bytes.Buffer + m := v.(map[string]interface{}) + buf.WriteString(fmt.Sprintf("%s-", m["name"].(string))) + buf.WriteString(fmt.Sprintf("%s-", m["protocol"].(string))) + buf.WriteString(fmt.Sprintf("%d-", m["port"].(int))) + buf.WriteString(fmt.Sprintf("%d-", m["local_port"].(int))) + + return hashcode.String(buf.String()) +} diff --git a/builtin/providers/azure/resource_virtual_machine_test.go b/builtin/providers/azure/resource_virtual_machine_test.go new file mode 100644 index 000000000..c519383d2 --- /dev/null +++ b/builtin/providers/azure/resource_virtual_machine_test.go @@ -0,0 +1,180 @@ +package azure + +import ( + "fmt" + "math/rand" + "testing" + "time" + + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" + "github.com/MSOpenTech/azure-sdk-for-go/clients/vmClient" +) + +func TestAccAzureVirtualMachine_Basic(t *testing.T) { + var VMDeployment vmClient.VMDeployment + + // The VM name can only be used once globally within azure, + // so we need to generate a random one + rand.Seed(time.Now().UnixNano()) + vmName := fmt.Sprintf("tf-test-vm-%d", rand.Int31()) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAzureVirtualMachineDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccCheckAzureVirtualMachineConfig_basic(vmName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAzureVirtualMachineExists("azure_virtual_machine.foobar", &VMDeployment), + testAccCheckAzureVirtualMachineAttributes(&VMDeployment, vmName), + resource.TestCheckResourceAttr( + "azure_virtual_machine.foobar", "name", vmName), + resource.TestCheckResourceAttr( + "azure_virtual_machine.foobar", "location", "West US"), + resource.TestCheckResourceAttr( + "azure_virtual_machine.foobar", "image", "b39f27a8b8c64d52b05eac6a62ebad85__Ubuntu-14_04-LTS-amd64-server-20140724-en-us-30GB"), + resource.TestCheckResourceAttr( + "azure_virtual_machine.foobar", "size", "Basic_A1"), + resource.TestCheckResourceAttr( + "azure_virtual_machine.foobar", "username", "foobar"), + ), + }, + }, + }) +} + +func TestAccAzureVirtualMachine_Endpoints(t *testing.T) { + var VMDeployment vmClient.VMDeployment + + // The VM name can only be used once globally within azure, + // so we need to generate a random one + rand.Seed(time.Now().UnixNano()) + vmName := fmt.Sprintf("tf-test-vm-%d", rand.Int31()) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAzureVirtualMachineDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccCheckAzureVirtualMachineConfig_endpoints(vmName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAzureVirtualMachineExists("azure_virtual_machine.foobar", &VMDeployment), + testAccCheckAzureVirtualMachineAttributes(&VMDeployment, vmName), + testAccCheckAzureVirtualMachineEndpoint(&VMDeployment, "tcp", 80), + ), + }, + }, + }) +} + +func testAccCheckAzureVirtualMachineDestroy(s *terraform.State) error { + for _, rs := range s.RootModule().Resources { + if rs.Type != "azure_virtual_machine" { + continue + } + + _, err := vmClient.GetVMDeployment(rs.Primary.ID, rs.Primary.ID) + if err == nil { + return fmt.Errorf("Azure Virtual Machine (%s) still exists", rs.Primary.ID) + } + } + + return nil +} + +func testAccCheckAzureVirtualMachineExists(n string, VMDeployment *vmClient.VMDeployment) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No Azure Virtual Machine ID is set") + } + + retrieveVMDeployment, err := vmClient.GetVMDeployment(rs.Primary.ID, rs.Primary.ID) + if err != nil { + return err + } + + if retrieveVMDeployment.Name != rs.Primary.ID { + return fmt.Errorf("Azure Virtual Machine not found %s %s", VMDeployment.Name, rs.Primary.ID) + } + + *VMDeployment = *retrieveVMDeployment + + return nil + } +} + +func testAccCheckAzureVirtualMachineAttributes(VMDeployment *vmClient.VMDeployment, vmName string) resource.TestCheckFunc { + return func(s *terraform.State) error { + if VMDeployment.Name != vmName { + return fmt.Errorf("Bad name: %s != %s", VMDeployment.Name, vmName) + } + + return nil + } +} + +func testAccCheckAzureVirtualMachineEndpoint(VMDeployment *vmClient.VMDeployment, protocol string, publicPort int) resource.TestCheckFunc { + return func(s *terraform.State) error { + roleInstances := VMDeployment.RoleInstanceList.RoleInstance + if len(roleInstances) == 0 { + return fmt.Errorf("Azure virtual machine does not have role instances") + } + + for i := 0; i < len(roleInstances); i++ { + instanceEndpoints := roleInstances[i].InstanceEndpoints.InstanceEndpoint + if len(instanceEndpoints) == 0 { + return fmt.Errorf("Azure virtual machine does not have endpoints") + } + endpointFound := 0 + for j := 0; i < len(instanceEndpoints); i++ { + if instanceEndpoints[j].Protocol == protocol && instanceEndpoints[j].PublicPort == publicPort { + endpointFound = 1 + break + } + } + if endpointFound == 0 { + return fmt.Errorf("Azure virtual machine does not have endpoint %s/%d", protocol, publicPort) + } + } + + return nil + } +} + +func testAccCheckAzureVirtualMachineConfig_basic(vmName string) string { + return fmt.Sprintf(` +resource "azure_virtual_machine" "foobar" { + name = "%s" + location = "West US" + image = "b39f27a8b8c64d52b05eac6a62ebad85__Ubuntu-14_04-LTS-amd64-server-20140724-en-us-30GB" + size = "Basic_A1" + username = "foobar" +} +`, vmName) +} + +func testAccCheckAzureVirtualMachineConfig_endpoints(vmName string) string { + return fmt.Sprintf(` +resource "azure_virtual_machine" "foobar" { + name = "%s" + location = "West US" + image = "b39f27a8b8c64d52b05eac6a62ebad85__Ubuntu-14_04-LTS-amd64-server-20140724-en-us-30GB" + size = "Basic_A1" + username = "foobar" + endpoint { + name = "http" + protocol = "tcp" + port = 80 + local_port = 80 + } +} +`, vmName) +} diff --git a/website/source/assets/stylesheets/_docs.scss b/website/source/assets/stylesheets/_docs.scss index a0d2ce807..cb1686a6e 100755 --- a/website/source/assets/stylesheets/_docs.scss +++ b/website/source/assets/stylesheets/_docs.scss @@ -16,6 +16,7 @@ body.layout-heroku, body.layout-mailgun, body.layout-digitalocean, body.layout-aws, +body.layout-azure, body.layout-docs, body.layout-inner, body.layout-downloads, diff --git a/website/source/docs/providers/azure/index.html.markdown b/website/source/docs/providers/azure/index.html.markdown new file mode 100644 index 000000000..4991ae632 --- /dev/null +++ b/website/source/docs/providers/azure/index.html.markdown @@ -0,0 +1,37 @@ +--- +layout: "azure" +page_title: "Provider: Microsoft Azure" +sidebar_current: "docs-azure-index" +description: |- + The Azure provider is used to interact with Microsoft Azure services. The provider needs to be configured with the proper credentials before it can be used. +--- + +# Azure Provider + +The Azure provider is used to interact with +[Microsoft Azure](http://azure.microsoft.com/). The provider needs +to be configured with the proper credentials before it can be used. + +Use the navigation to the left to read about the available resources. + +## Example Usage + +``` +# Configure the Azure provider +provider "azure" { + publish_settings_file = "account.publishsettings" +} + +# Create a new instance +resource "azure_virtual_machine" "default" { + ... +} +``` + +## Argument Reference + +The following keys can be used to configure the provider. + +* `publish_settings_file` - (Required) Path to the JSON file used to describe + your account settings, downloaded from Microsoft Azure. It must be provided, + but it can also be sourced from the AZURE_PUBLISH_SETTINGS_FILE environment variable. diff --git a/website/source/docs/providers/azure/r/virtual_machine.html.markdown b/website/source/docs/providers/azure/r/virtual_machine.html.markdown new file mode 100644 index 000000000..946f3b11d --- /dev/null +++ b/website/source/docs/providers/azure/r/virtual_machine.html.markdown @@ -0,0 +1,71 @@ +--- +layout: "azure" +page_title: "Azure: azure_virtual_machine" +sidebar_current: "docs-azure-resource-virtual-machine" +description: |- + Manages a Virtual Machine resource within Azure. +--- + +# azure\_virtual\_machine + +Manages a Virtual Machine resource within Azure. + +## Example Usage + +``` +resource "azure_virtual_machine" "default" { + name = "test" + location = "West US" + image = "b39f27a8b8c64d52b05eac6a62ebad85__Ubuntu-14_04-LTS-amd64-server-20140724-en-us-30GB" + size = "Basic_A1" + username = "${var.username}" + password = ""${var.password}" + ssh_public_key_file = "${var.azure_ssh_public_key_file}" + endpoint { + name = "http" + protocol = "tcp" + port = 80 + local_port = 80 + } +} +``` + +## Argument Reference + +The following arguments are supported: + +* `name` - (Required) A name for the virtual machine. It must use between 3 and + 24 lowercase letters and numbers and it must be unique within Azure. + +* `location` - (Required) The location that the virtual machine should be created in. + +* `image` - (Required) A image to be used to create the virtual machine. + +* `size` - (Required) Size that you want to use for the virtual machine. + +* `username` - (Required) Name of the account that you will use to administer + the virtual machine. You cannot use root for the user name. + +* `password` - (Optional) Password for the admin account. + +* `ssh_public_key_file` - (Optional) SSH key (PEM format). + +* `ssh_port` - (Optional) SSH port. + +* `endpoint` - (Optional) Can be specified multiple times for each + endpoint rule. Each endpoint block supports fields documented below. + +The `endpoint` block supports: + +* `name` - (Required) The name of the endpoint. +* `protocol` - (Required) The protocol. +* `port` - (Required) The public port. +* `local_port` - (Required) The private port. + +## Attributes Reference + +The following attributes are exported: + +* `url` - The URL for the virtual machine deployment. +* `ip_address` - The internal IP address of the virtual machine. +* `vip_address` - The public Virtual IP address of the virtual machine. diff --git a/website/source/layouts/azure.erb b/website/source/layouts/azure.erb new file mode 100644 index 000000000..918a12469 --- /dev/null +++ b/website/source/layouts/azure.erb @@ -0,0 +1,26 @@ +<% wrap_layout :inner do %> + <% content_for :sidebar do %> +
+ <% end %> + + <%= yield %> +<% end %> diff --git a/website/source/layouts/docs.erb b/website/source/layouts/docs.erb index c71ac5a2e..8e07b6104 100644 --- a/website/source/layouts/docs.erb +++ b/website/source/layouts/docs.erb @@ -112,6 +112,10 @@ AWS +