Merge pull request #1721 from managedbyq/aws_dhcp_options

providers/aws: Implements DHCP Options Set support.
This commit is contained in:
Paul Hinze 2015-04-30 17:13:15 -05:00
commit d230cfb907
9 changed files with 718 additions and 6 deletions

View File

@ -105,6 +105,8 @@ func Provider() terraform.ResourceProvider {
"aws_subnet": resourceAwsSubnet(),
"aws_vpc": resourceAwsVpc(),
"aws_vpc_peering_connection": resourceAwsVpcPeeringConnection(),
"aws_vpc_dhcp_options": resourceAwsVpcDhcpOptions(),
"aws_vpc_dhcp_options_association": resourceAwsVpcDhcpOptionsAssociation(),
"aws_vpn_gateway": resourceAwsVpnGateway(),
},

View File

@ -53,6 +53,11 @@ func resourceAwsVpc() *schema.Resource {
Computed: true,
},
"dhcp_options_id": &schema.Schema{
Type: schema.TypeString,
Computed: true,
},
"default_security_group_id": &schema.Schema{
Type: schema.TypeString,
Computed: true,
@ -126,6 +131,7 @@ func resourceAwsVpcRead(d *schema.ResourceData, meta interface{}) error {
vpc := vpcRaw.(*ec2.VPC)
vpcid := d.Id()
d.Set("cidr_block", vpc.CIDRBlock)
d.Set("dhcp_options_id", vpc.DHCPOptionsID)
// Tags
d.Set("tags", tagsToMapSDK(vpc.Tags))

View File

@ -0,0 +1,280 @@
package aws
import (
"fmt"
"log"
"strings"
"time"
"github.com/awslabs/aws-sdk-go/aws"
"github.com/awslabs/aws-sdk-go/service/ec2"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/helper/schema"
)
func resourceAwsVpcDhcpOptions() *schema.Resource {
return &schema.Resource{
Create: resourceAwsVpcDhcpOptionsCreate,
Read: resourceAwsVpcDhcpOptionsRead,
Update: resourceAwsVpcDhcpOptionsUpdate,
Delete: resourceAwsVpcDhcpOptionsDelete,
Schema: map[string]*schema.Schema{
"domain_name": &schema.Schema{
Type: schema.TypeString,
Optional: true,
ForceNew: true,
},
"domain_name_servers": &schema.Schema{
Type: schema.TypeList,
Optional: true,
ForceNew: true,
Elem: &schema.Schema{Type: schema.TypeString},
},
"ntp_servers": &schema.Schema{
Type: schema.TypeList,
Optional: true,
ForceNew: true,
Elem: &schema.Schema{Type: schema.TypeString},
},
"netbios_node_type": &schema.Schema{
Type: schema.TypeString,
Optional: true,
ForceNew: true,
},
"netbios_name_servers": &schema.Schema{
Type: schema.TypeList,
Optional: true,
ForceNew: true,
Elem: &schema.Schema{Type: schema.TypeString},
},
"tags": &schema.Schema{
Type: schema.TypeMap,
Optional: true,
},
},
}
}
func resourceAwsVpcDhcpOptionsCreate(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).ec2conn
setDHCPOption := func(key string) *ec2.NewDHCPConfiguration {
log.Printf("[DEBUG] Setting DHCP option %s...", key)
tfKey := strings.Replace(key, "-", "_", -1)
value, ok := d.GetOk(tfKey)
if !ok {
return nil
}
if v, ok := value.(string); ok {
return &ec2.NewDHCPConfiguration{
Key: aws.String(key),
Values: []*string{
aws.String(v),
},
}
}
if v, ok := value.([]interface{}); ok {
var s []*string
for _, attr := range v {
s = append(s, aws.String(attr.(string)))
}
return &ec2.NewDHCPConfiguration{
Key: aws.String(key),
Values: s,
}
}
return nil
}
createOpts := &ec2.CreateDHCPOptionsInput{
DHCPConfigurations: []*ec2.NewDHCPConfiguration{
setDHCPOption("domain-name"),
setDHCPOption("domain-name-servers"),
setDHCPOption("ntp-servers"),
setDHCPOption("netbios-node-type"),
setDHCPOption("netbios-name-servers"),
},
}
resp, err := conn.CreateDHCPOptions(createOpts)
if err != nil {
return fmt.Errorf("Error creating DHCP Options Set: %s", err)
}
dos := resp.DHCPOptions
d.SetId(*dos.DHCPOptionsID)
log.Printf("[INFO] DHCP Options Set ID: %s", d.Id())
// Wait for the DHCP Options to become available
log.Printf("[DEBUG] Waiting for DHCP Options (%s) to become available", d.Id())
stateConf := &resource.StateChangeConf{
Pending: []string{"pending"},
Target: "",
Refresh: DHCPOptionsStateRefreshFunc(conn, d.Id()),
Timeout: 1 * time.Minute,
}
if _, err := stateConf.WaitForState(); err != nil {
return fmt.Errorf(
"Error waiting for DHCP Options (%s) to become available: %s",
d.Id(), err)
}
return resourceAwsVpcDhcpOptionsUpdate(d, meta)
}
func resourceAwsVpcDhcpOptionsRead(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).ec2conn
req := &ec2.DescribeDHCPOptionsInput{
DHCPOptionsIDs: []*string{
aws.String(d.Id()),
},
}
resp, err := conn.DescribeDHCPOptions(req)
if err != nil {
return fmt.Errorf("Error retrieving DHCP Options: %s", err)
}
if len(resp.DHCPOptions) == 0 {
return nil
}
opts := resp.DHCPOptions[0]
d.Set("tags", tagsToMapSDK(opts.Tags))
for _, cfg := range opts.DHCPConfigurations {
tfKey := strings.Replace(*cfg.Key, "-", "_", -1)
if _, ok := d.Get(tfKey).(string); ok {
d.Set(tfKey, cfg.Values[0].Value)
} else {
values := make([]string, 0, len(cfg.Values))
for _, v := range cfg.Values {
values = append(values, *v.Value)
}
d.Set(tfKey, values)
}
}
return nil
}
func resourceAwsVpcDhcpOptionsUpdate(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).ec2conn
return setTagsSDK(conn, d)
}
func resourceAwsVpcDhcpOptionsDelete(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).ec2conn
return resource.Retry(3*time.Minute, func() error {
log.Printf("[INFO] Deleting DHCP Options ID %s...", d.Id())
_, err := conn.DeleteDHCPOptions(&ec2.DeleteDHCPOptionsInput{
DHCPOptionsID: aws.String(d.Id()),
})
if err == nil {
return nil
}
log.Printf("[WARN] %s", err)
ec2err, ok := err.(aws.APIError)
if !ok {
return err
}
switch ec2err.Code {
case "InvalidDhcpOptionsID.NotFound":
return nil
case "DependencyViolation":
// If it is a dependency violation, we want to disassociate
// all VPCs using the given DHCP Options ID, and retry deleting.
vpcs, err2 := findVPCsByDHCPOptionsID(conn, d.Id())
if err2 != nil {
log.Printf("[ERROR] %s", err2)
return err2
}
for _, vpc := range vpcs {
log.Printf("[INFO] Disassociating DHCP Options Set %s from VPC %s...", d.Id(), *vpc.VPCID)
if _, err := conn.AssociateDHCPOptions(&ec2.AssociateDHCPOptionsInput{
DHCPOptionsID: aws.String("default"),
VPCID: vpc.VPCID,
}); err != nil {
return err
}
}
return err //retry
default:
// Any other error, we want to quit the retry loop immediately
return resource.RetryError{Err: err}
}
return nil
})
}
func findVPCsByDHCPOptionsID(conn *ec2.EC2, id string) ([]*ec2.VPC, error) {
req := &ec2.DescribeVPCsInput{
Filters: []*ec2.Filter{
&ec2.Filter{
Name: aws.String("dhcp-options-id"),
Values: []*string{
aws.String(id),
},
},
},
}
resp, err := conn.DescribeVPCs(req)
if err != nil {
if ec2err, ok := err.(aws.APIError); ok && ec2err.Code == "InvalidVpcID.NotFound" {
return nil, nil
}
return nil, err
}
return resp.VPCs, nil
}
func DHCPOptionsStateRefreshFunc(conn *ec2.EC2, id string) resource.StateRefreshFunc {
return func() (interface{}, string, error) {
DescribeDhcpOpts := &ec2.DescribeDHCPOptionsInput{
DHCPOptionsIDs: []*string{
aws.String(id),
},
}
resp, err := conn.DescribeDHCPOptions(DescribeDhcpOpts)
if err != nil {
if ec2err, ok := err.(aws.APIError); ok && ec2err.Code == "InvalidDhcpOptionsID.NotFound" {
resp = nil
} else {
log.Printf("Error on DHCPOptionsStateRefresh: %s", err)
return nil, "", err
}
}
if resp == nil {
// Sometimes AWS just has consistency issues and doesn't see
// our instance yet. Return an empty state.
return nil, "", nil
}
dos := resp.DHCPOptions[0]
return dos, "", nil
}
}

View File

@ -0,0 +1,99 @@
package aws
import (
"log"
"github.com/awslabs/aws-sdk-go/aws"
"github.com/awslabs/aws-sdk-go/service/ec2"
"github.com/hashicorp/terraform/helper/schema"
)
func resourceAwsVpcDhcpOptionsAssociation() *schema.Resource {
return &schema.Resource{
Create: resourceAwsVpcDhcpOptionsAssociationCreate,
Read: resourceAwsVpcDhcpOptionsAssociationRead,
Update: resourceAwsVpcDhcpOptionsAssociationUpdate,
Delete: resourceAwsVpcDhcpOptionsAssociationDelete,
Schema: map[string]*schema.Schema{
"vpc_id": &schema.Schema{
Type: schema.TypeString,
Required: true,
},
"dhcp_options_id": &schema.Schema{
Type: schema.TypeString,
Required: true,
},
},
}
}
func resourceAwsVpcDhcpOptionsAssociationCreate(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).ec2conn
log.Printf(
"[INFO] Creating DHCP Options association: %s => %s",
d.Get("vpc_id").(string),
d.Get("dhcp_options_id").(string))
optsID := aws.String(d.Get("dhcp_options_id").(string))
vpcID := aws.String(d.Get("vpc_id").(string))
if _, err := conn.AssociateDHCPOptions(&ec2.AssociateDHCPOptionsInput{
DHCPOptionsID: optsID,
VPCID: vpcID,
}); err != nil {
return err
}
// Set the ID and return
d.SetId(*optsID + "-" + *vpcID)
log.Printf("[INFO] Association ID: %s", d.Id())
return nil
}
func resourceAwsVpcDhcpOptionsAssociationRead(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).ec2conn
// Get the VPC that this association belongs to
vpcRaw, _, err := VPCStateRefreshFunc(conn, d.Get("vpc_id").(string))()
if err != nil {
return err
}
if vpcRaw == nil {
return nil
}
vpc := vpcRaw.(*ec2.VPC)
if *vpc.VPCID != d.Get("vpc_id") || *vpc.DHCPOptionsID != d.Get("dhcp_options_id") {
log.Printf("[INFO] It seems the DHCP Options association is gone. Deleting reference from Graph...")
d.SetId("")
}
return nil
}
// DHCP Options Asociations cannot be updated.
func resourceAwsVpcDhcpOptionsAssociationUpdate(d *schema.ResourceData, meta interface{}) error {
return resourceAwsVpcDhcpOptionsAssociationCreate(d, meta)
}
// AWS does not provide an API to disassociate a DHCP Options set from a VPC.
// So, we do this by setting the VPC to the default DHCP Options Set.
func resourceAwsVpcDhcpOptionsAssociationDelete(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).ec2conn
log.Printf("[INFO] Disassociating DHCP Options Set %s from VPC %s...", d.Get("dhcp_options_id"), d.Get("vpc_id"))
if _, err := conn.AssociateDHCPOptions(&ec2.AssociateDHCPOptionsInput{
DHCPOptionsID: aws.String("default"),
VPCID: aws.String(d.Get("vpc_id").(string)),
}); err != nil {
return err
}
d.SetId("")
return nil
}

View File

@ -0,0 +1,99 @@
package aws
import (
"fmt"
"testing"
"github.com/awslabs/aws-sdk-go/service/ec2"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform"
)
func TestAccAWSDHCPOptionsAssociation(t *testing.T) {
var v ec2.VPC
var d ec2.DHCPOptions
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckDHCPOptionsAssociationDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccDHCPOptionsAssociationConfig,
Check: resource.ComposeTestCheckFunc(
testAccCheckDHCPOptionsExists("aws_vpc_dhcp_options.foo", &d),
testAccCheckVpcExists("aws_vpc.foo", &v),
testAccCheckDHCPOptionsAssociationExist("aws_vpc_dhcp_options_association.foo", &v),
),
},
},
})
}
func testAccCheckDHCPOptionsAssociationDestroy(s *terraform.State) error {
conn := testAccProvider.Meta().(*AWSClient).ec2conn
for _, rs := range s.RootModule().Resources {
if rs.Type != "aws_vpc_dhcp_options_association" {
continue
}
// Try to find the VPC associated to the DHCP Options set
vpcs, err := findVPCsByDHCPOptionsID(conn, rs.Primary.Attributes["dhcp_options_id"])
if err != nil {
return err
}
if len(vpcs) > 0 {
return fmt.Errorf("DHCP Options association is still associated to %d VPCs.", len(vpcs))
}
}
return nil
}
func testAccCheckDHCPOptionsAssociationExist(n string, vpc *ec2.VPC) 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 DHCP Options Set association ID is set")
}
if *vpc.DHCPOptionsID != rs.Primary.Attributes["dhcp_options_id"] {
return fmt.Errorf("VPC %s does not have DHCP Options Set %s associated", *vpc.VPCID, rs.Primary.Attributes["dhcp_options_id"])
}
if *vpc.VPCID != rs.Primary.Attributes["vpc_id"] {
return fmt.Errorf("DHCP Options Set %s is not associated with VPC %s", rs.Primary.Attributes["dhcp_options_id"], *vpc.VPCID)
}
return nil
}
}
const testAccDHCPOptionsAssociationConfig = `
resource "aws_vpc" "foo" {
cidr_block = "10.1.0.0/16"
}
resource "aws_vpc_dhcp_options" "foo" {
domain_name = "service.consul"
domain_name_servers = ["127.0.0.1", "10.0.0.2"]
ntp_servers = ["127.0.0.1"]
netbios_name_servers = ["127.0.0.1"]
netbios_node_type = 2
tags {
Name = "foo"
}
}
resource "aws_vpc_dhcp_options_association" "foo" {
vpc_id = "${aws_vpc.foo.id}"
dhcp_options_id = "${aws_vpc_dhcp_options.foo.id}"
}
`

View File

@ -0,0 +1,115 @@
package aws
import (
"fmt"
"testing"
"github.com/awslabs/aws-sdk-go/aws"
"github.com/awslabs/aws-sdk-go/service/ec2"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform"
)
func TestAccDHCPOptions(t *testing.T) {
var d ec2.DHCPOptions
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckDHCPOptionsDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccDHCPOptionsConfig,
Check: resource.ComposeTestCheckFunc(
testAccCheckDHCPOptionsExists("aws_vpc_dhcp_options.foo", &d),
resource.TestCheckResourceAttr("aws_vpc_dhcp_options.foo", "domain_name", "service.consul"),
resource.TestCheckResourceAttr("aws_vpc_dhcp_options.foo", "domain_name_servers.0", "127.0.0.1"),
resource.TestCheckResourceAttr("aws_vpc_dhcp_options.foo", "domain_name_servers.1", "10.0.0.2"),
resource.TestCheckResourceAttr("aws_vpc_dhcp_options.foo", "ntp_servers.0", "127.0.0.1"),
resource.TestCheckResourceAttr("aws_vpc_dhcp_options.foo", "netbios_name_servers.0", "127.0.0.1"),
resource.TestCheckResourceAttr("aws_vpc_dhcp_options.foo", "netbios_node_type", "2"),
resource.TestCheckResourceAttr("aws_vpc_dhcp_options.foo", "tags.Name", "foo-name"),
),
},
},
})
}
func testAccCheckDHCPOptionsDestroy(s *terraform.State) error {
conn := testAccProvider.Meta().(*AWSClient).ec2conn
for _, rs := range s.RootModule().Resources {
if rs.Type != "aws_vpc_dhcp_options" {
continue
}
// Try to find the resource
resp, err := conn.DescribeDHCPOptions(&ec2.DescribeDHCPOptionsInput{
DHCPOptionsIDs: []*string{
aws.String(rs.Primary.ID),
},
})
if err == nil {
if len(resp.DHCPOptions) > 0 {
return fmt.Errorf("still exist.")
}
return nil
}
// Verify the error is what we want
ec2err, ok := err.(aws.APIError)
if !ok {
return err
}
if ec2err.Code != "InvalidDhcpOptionsID.NotFound" {
return err
}
}
return nil
}
func testAccCheckDHCPOptionsExists(n string, d *ec2.DHCPOptions) 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 ID is set")
}
conn := testAccProvider.Meta().(*AWSClient).ec2conn
resp, err := conn.DescribeDHCPOptions(&ec2.DescribeDHCPOptionsInput{
DHCPOptionsIDs: []*string{
aws.String(rs.Primary.ID),
},
})
if err != nil {
return err
}
if len(resp.DHCPOptions) == 0 {
return fmt.Errorf("DHCP Options not found")
}
*d = *resp.DHCPOptions[0]
return nil
}
}
const testAccDHCPOptionsConfig = `
resource "aws_vpc_dhcp_options" "foo" {
domain_name = "service.consul"
domain_name_servers = ["127.0.0.1", "10.0.0.2"]
ntp_servers = ["127.0.0.1"]
netbios_name_servers = ["127.0.0.1"]
netbios_node_type = 2
tags {
Name = "foo-name"
}
}
`

View File

@ -0,0 +1,66 @@
---
layout: "aws"
page_title: "AWS: aws_vpc_dhcp_options"
sidebar_current: "docs-aws-resource-vpc-dhcp-options"
description: |-
Provides a VPC DHCP Options resource.
---
# aws\_vpc\_dhcp\_options
Provides a VPC DHCP Options resource.
## Example Usage
Basic usage:
```
resource "aws_vpc_dhcp_options" "dns_resolver" {
domain_name_servers = ["8.8.8.8", "8.8.4.4"]
}
```
Full usage:
```
resource "aws_vpc_dhcp_options" "foo" {
domain_name = "service.consul"
domain_name_servers = ["127.0.0.1", "10.0.0.2"]
ntp_servers = ["127.0.0.1"]
netbios_name_servers = ["127.0.0.1"]
netbios_node_type = 2
tags {
Name = "foo-name"
}
}
```
## Argument Reference
The following arguments are supported:
* `domain_name` - (Optional) the suffix domain name to use by default when resolving non Fully Qualified Domain Names. In other words, this is what ends up being the `search` value in the `/etc/resolv.conf` file.
* `domain_name_servers` - (Optional) List of name servers to configure in `/etc/resolv.conf`.
* `ntp_servers` - (Optional) List of NTP servers to configure.
* `netbios_name_servers` - (Optional) List of NETBIOS name servers.
* `netbios_node_type` - (Optional) The NetBIOS node type (1, 2, 4, or 8). AWS recommends to specify 2 since broadcast and multicast are not supported in their network. For more information about these node types, see [RFC 2132](http://www.ietf.org/rfc/rfc2132.txt).
* `tags` - (Optional) A mapping of tags to assign to the resource.
## Remarks
* Notice that all arguments are optional but you have to specify at least one argument.
* `domain_name_servers`, `netbios_name_servers`, `ntp_servers` are limited by AWS to maximum four servers only.
* To actually use the DHCP Options Set you need to associate it to a VPC using [`aws_vpc_dhcp_options_association`](/docs/providers/aws/r/vpc_dhcp_options_association.html).
* If you delete a DHCP Options Set, all VPCs using it will be associated to AWS's `default` DHCP Option Set.
## Attributes Reference
The following attributes are exported:
* `id` - The ID of the DHCP Options Set.
## Known Issues
* https://github.com/awslabs/aws-sdk-go/issues/210
You can find more technical documentation about DHCP Options Set in the
official [AWS User Guide](http://docs.aws.amazon.com/AmazonVPC/latest/UserGuide/VPC_DHCP_Options.html).

View File

@ -0,0 +1,37 @@
---
layout: "aws"
page_title: "AWS: aws_vpc_dhcp_options_association"
sidebar_current: "docs-aws-resource-vpc-dhcp-options-association"
description: |-
Provides a VPC DHCP Options Association resource.
---
# aws\_vpc\_dhcp\_options\_<wbr>association
Provides a VPC DHCP Options Association resource.
## Example Usage
```
resource "aws_vpc_dhcp_options_association" "dns_resolver" {
vpc_id = "${aws_vpc.foo.id}"
dhcp_options_id = "${aws_vpc_dhcp_options.foo.id}"
}
```
## Argument Reference
The following arguments are supported:
* `vpc_id` - (Required) The ID of the VPC to which we would like to associate a DHCP Options Set.
* `dhcp_options_id` - (Required) The ID of the DHCP Options Set to associate to the VPC.
## Remarks
* You can only associate one DHCP Options Set to a given VPC ID.
* Removing the DHCP Options Association automatically sets AWS's `default` DHCP Options Set to the VPC.
## Attributes Reference
The following attributes are exported:
* `id` - The ID of the DHCP Options Set Association.

View File

@ -110,10 +110,18 @@
<li<%= sidebar_current("docs-aws-resource-vpc-peering") %>>
<a href="/docs/providers/aws/r/vpc_peering.html">aws_vpc_peering</a>
</li>
<li<%= sidebar_current("docs-aws-resource-vpc-dhcp-options") %>>
<a href="/docs/providers/aws/r/vpc_dhcp_options.html">aws_vpc_dhcp_options</a>
</li>
<li<%= sidebar_current("docs-aws-resource-vpc-dhcp-options-association") %>>
<a href="/docs/providers/aws/r/vpc_dhcp_options_association.html">aws_vpc_dhcp_options_association</a>
</li>
<li<%= sidebar_current("docs-aws-resource-vpn-gateway") %>>
<a href="/docs/providers/aws/r/vpn_gateway.html">aws_vpn_gateway</a>
</li>
</ul>
</li>