Merge pull request #899 from hashicorp/770-azure-provider

Add Azure provider
This commit is contained in:
Paul Hinze 2015-01-30 16:36:51 -06:00
commit 8749d0090a
12 changed files with 686 additions and 0 deletions

View File

@ -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,
})
}

View File

@ -0,0 +1 @@
package main

View File

@ -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
}

View File

@ -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
}

View File

@ -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")
}
}

View File

@ -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())
}

View File

@ -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)
}

View File

@ -16,6 +16,7 @@ body.layout-heroku,
body.layout-mailgun, body.layout-mailgun,
body.layout-digitalocean, body.layout-digitalocean,
body.layout-aws, body.layout-aws,
body.layout-azure,
body.layout-docs, body.layout-docs,
body.layout-inner, body.layout-inner,
body.layout-downloads, body.layout-downloads,

View File

@ -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.

View File

@ -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.

View File

@ -0,0 +1,26 @@
<% wrap_layout :inner do %>
<% content_for :sidebar do %>
<div class="docs-sidebar hidden-print affix-top" role="complementary">
<ul class="nav docs-sidenav">
<li<%= sidebar_current("docs-home") %>>
<a href="/docs/index.html">&laquo; Documentation Home</a>
</li>
<li<%= sidebar_current("docs-azure-index") %>>
<a href="/docs/providers/azure/index.html">Azure Provider</a>
</li>
<li<%= sidebar_current("docs-azure-resource") %>>
<a href="#">Resources</a>
<ul class="nav nav-visible">
<li<%= sidebar_current("docs-azure-resource-virtual-machine") %>>
<a href="/docs/providers/azure/r/virtual_machine.html">azure_virtual_machine</a>
</li>
</ul>
</li>
</ul>
</div>
<% end %>
<%= yield %>
<% end %>

View File

@ -112,6 +112,10 @@
<a href="/docs/providers/aws/index.html">AWS</a> <a href="/docs/providers/aws/index.html">AWS</a>
</li> </li>
<li<%= sidebar_current("docs-providers-azure") %>>
<a href="/docs/providers/azure/index.html">Azure</a>
</li>
<li<%= sidebar_current("docs-providers-cloudflare") %>> <li<%= sidebar_current("docs-providers-cloudflare") %>>
<a href="/docs/providers/cloudflare/index.html">CloudFlare</a> <a href="/docs/providers/cloudflare/index.html">CloudFlare</a>
</li> </li>