Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature subnet #156

Closed
wants to merge 12 commits into from
Next Next commit
Initial implementation of subnet resource.
  • Loading branch information
minsikl committed Jun 15, 2017
commit 86b1cc06e7b2c765f087cfffaffcb5ca8cce802f
1 change: 1 addition & 0 deletions softlayer/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ func Provider() terraform.ResourceProvider {
"softlayer_file_storage": resourceSoftLayerFileStorage(),
"softlayer_block_storage": resourceSoftLayerBlockStorage(),
"softlayer_dns_secondary": resourceSoftLayerDnsSecondary(),
"softlayer_subnet": resourceSoftLayerSubnet(),
},

ConfigureFunc: providerConfigure,
Expand Down
369 changes: 369 additions & 0 deletions softlayer/resource_softlayer_subnet.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,369 @@
package softlayer

import (
"errors"
"fmt"
"log"
"strconv"
"strings"
"time"

"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/helper/schema"
"github.com/softlayer/softlayer-go/datatypes"
"github.com/softlayer/softlayer-go/filter"
"github.com/softlayer/softlayer-go/helpers/product"
"github.com/softlayer/softlayer-go/services"
"github.com/softlayer/softlayer-go/session"
"github.com/softlayer/softlayer-go/sl"
)

const (
SubnetMask = "id,addressSpace,subnetType,version,ipAddressCount," +
"broadcastAddress,cidr,note,endPointIpAddress[ipAddress],networkVlan[id]"
)

var (
// Map subnet types to product package keyname in SoftLayer_Product_Item
subnetPackageTypeMap = map[string]string{
"STATIC_IP_ROUTED": "ADDITIONAL_SERVICES_STATIC_IP_ADDRESSES",
"ROUTED_TO_VLAN": "ADDITIONAL_SERVICES_PORTABLE_IP_ADDRESSES",
}
)

func resourceSoftLayerSubnet() *schema.Resource {
return &schema.Resource{
Create: resourceSoftLayerSubnetCreate,
Read: resourceSoftLayerSubnetRead,
Update: resourceSoftLayerSubnetUpdate,
Delete: resourceSoftLayerSubnetDelete,
Exists: resourceSoftLayerSubnetExists,
Importer: &schema.ResourceImporter{},

Schema: map[string]*schema.Schema{
"id": &schema.Schema{
Type: schema.TypeInt,
Computed: true,
},

"vlan_type": {
Type: schema.TypeString,
Computed: true,
},

"type": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
ValidateFunc: func(v interface{}, k string) (ws []string, errs []error) {
typeStr := v.(string)
if typeStr != "ROUTED_TO_VLAN" && typeStr != "STATIC_IP_ROUTED" {
errs = append(errs, errors.New(
"type should be either ROUTED_TO_VLAN or STATIC_IP_ROUTED."))
}
return
},
},

// IP version 4 or IP version 6
"ip_version": {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could we have a default of 4 for the ip version?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Type: schema.TypeInt,
Required: true,
ForceNew: true,
ValidateFunc: func(v interface{}, k string) (ws []string, errs []error) {
ipVersion := v.(int)
if ipVersion != 4 && ipVersion != 6 {
errs = append(errs, errors.New(
"ip version should be either 4 or 6."))
}
return
},
},

"capacity": {
Type: schema.TypeInt,
Required: true,
ForceNew: true,
},

"vlan_id": {
Type: schema.TypeInt,
Required: true,
ForceNew: true,
},

// endpoint_ip should be configured when type is "STATIC_IP_ROUTED"
"endpoint_ip": {
Type: schema.TypeString,
Optional: true,
ForceNew: true,
},

// Provides IP address/netmask format (ex. 10.10.10.10/28)
"name": &schema.Schema{
Type: schema.TypeString,
Computed: true,
},

"notes": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
},
}
}

func resourceSoftLayerSubnetCreate(d *schema.ResourceData, meta interface{}) error {
sess := meta.(ProviderConfig).SoftLayerSession()

// Find price items with AdditionalServicesSubnetAddresses
productOrderContainer, err := buildSubnetProductOrderContainer(d, sess)
if err != nil {
return fmt.Errorf("Error creating subnet: %s", err)
}

log.Println("[INFO] Creating subnet")

receipt, err := services.GetProductOrderService(sess).
PlaceOrder(productOrderContainer, sl.Bool(false))
if err != nil {
return fmt.Errorf("Error during creation of subnet: %s", err)
}

Subnet, err := findSubnetByOrderId(sess, *receipt.OrderId)
if err != nil {
return fmt.Errorf("Error during creation of subnet: %s", err)
}

d.SetId(fmt.Sprintf("%d", *Subnet.Id))

return resourceSoftLayerSubnetUpdate(d, meta)
}

func resourceSoftLayerSubnetRead(d *schema.ResourceData, meta interface{}) error {
sess := meta.(ProviderConfig).SoftLayerSession()
service := services.GetNetworkSubnetService(sess)

subnetId, err := strconv.Atoi(d.Id())
if err != nil {
return fmt.Errorf("Not a valid subnet ID, must be an integer: %s", err)
}

subnet, err := service.Id(subnetId).Mask(SubnetMask).GetObject()
if err != nil {
return fmt.Errorf("Error retrieving a subnet: %s", err)
}

d.Set("vlan_type", *subnet.AddressSpace)
d.Set("type", *subnet.SubnetType)
if *subnet.SubnetType == "SECONDARY_ON_VLAN" {
d.Set("type", "ROUTED_TO_VLAN")
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@minsikl There are SubnetTypes that would not contain STATIC or VLAN. In that case "type" would be unset. Is that intentional?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@renier Portable subnets contain "VLAN" and Static subnets contain "STATIC". In the future, additional types can be added. For example, Primary subnets contain "PRIMARY".

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@minsikl Since there are some types that do not contain either word, technically, would it would be possible to get that from the API? If so, programmatically, it would be safer to add an else clause to handle that situation. For example, to throw an error in that case, since we don't support any other type of subnet.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@renier Users may try to import different type of subnets. The current code will not generate an error. If you want to check the type strictly, you can throw an exception.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated the code to generate an error when an invalid subnet type is detected.

d.Set("ip_version", *subnet.Version)
d.Set("capacity", *subnet.IpAddressCount)
d.Set("name", *subnet.BroadcastAddress+"/"+strconv.Itoa(*subnet.Cidr))
if subnet.Note != nil {
d.Set("notes", *subnet.Note)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not necessary, but these blocks could be refactored with d.Set("notes", sl.Get(subnet.Note, nil))

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

if subnet.EndPointIpAddress != nil {
d.Set("endpoint_ip", *subnet.EndPointIpAddress.IpAddress)
}
if subnet.NetworkVlan != nil {
d.Set("vlan_id", *subnet.NetworkVlan.Id)
}
return nil
}

func resourceSoftLayerSubnetUpdate(d *schema.ResourceData, meta interface{}) error {
sess := meta.(ProviderConfig).SoftLayerSession()
service := services.GetNetworkSubnetService(sess)

subnetId, err := strconv.Atoi(d.Id())
if err != nil {
return fmt.Errorf("Not a valid subnet ID, must be an integer: %s", err)
}

if d.HasChange("notes") {
_, err = service.Id(subnetId).EditNote(sl.String(d.Get("notes").(string)))
if err != nil {
return fmt.Errorf("Error updating subnet: %s", err)
}
}
return resourceSoftLayerVlanRead(d, meta)
}

func resourceSoftLayerSubnetDelete(d *schema.ResourceData, meta interface{}) error {
sess := meta.(ProviderConfig).SoftLayerSession()
service := services.GetNetworkSubnetService(sess)

subnetId, err := strconv.Atoi(d.Id())
if err != nil {
return fmt.Errorf("Not a valid subnet ID, must be an integer: %s", err)
}

billingItem, err := service.Id(subnetId).GetBillingItem()
if err != nil {
return fmt.Errorf("Error deleting subnet: %s", err)
}

if billingItem.Id == nil {
return nil
}

_, err = services.GetBillingItemService(sess).Id(*billingItem.Id).CancelService()

return err
}

func resourceSoftLayerSubnetExists(d *schema.ResourceData, meta interface{}) (bool, error) {
sess := meta.(ProviderConfig).SoftLayerSession()
service := services.GetNetworkSubnetService(sess)

subnetId, err := strconv.Atoi(d.Id())
if err != nil {
return false, fmt.Errorf("Not a valid ID, must be an integer: %s", err)
}

result, err := service.Id(subnetId).GetObject()
if err != nil {
if apiErr, ok := err.(sl.Error); ok && apiErr.StatusCode == 404 {
return false, nil
}
return false, fmt.Errorf("Error retrieving subnet: %s", err)
}
return result.Id != nil && *result.Id == subnetId, nil
}

func findSubnetByOrderId(sess *session.Session, orderId int) (datatypes.Network_Subnet, error) {
stateConf := &resource.StateChangeConf{
Pending: []string{"pending"},
Target: []string{"complete"},
Refresh: func() (interface{}, string, error) {
subnets, err := services.GetAccountService(sess).
Filter(filter.Path("subnets.billingItem.orderItem.order.id").
Eq(strconv.Itoa(orderId)).Build()).
Mask("id").
GetSubnets()
if err != nil {
return datatypes.Network_Subnet{}, "", err
}

if len(subnets) == 1 {
return subnets[0], "complete", nil
}
return nil, "pending", nil
},
Timeout: 10 * time.Minute,
Delay: 5 * time.Second,
MinTimeout: 3 * time.Second,
}

pendingResult, err := stateConf.WaitForState()

if err != nil {
return datatypes.Network_Subnet{}, err
}

if result, ok := pendingResult.(datatypes.Network_Subnet); ok {
return result, nil
}

return datatypes.Network_Subnet{},
fmt.Errorf("Cannot find a subnet with order id '%d'", orderId)
}

func buildSubnetProductOrderContainer(d *schema.ResourceData, sess *session.Session) (
*datatypes.Container_Product_Order_Network_Subnet, error) {

// 1. Get a package
typeStr := d.Get("type").(string)
pkg, err := product.GetPackageByType(sess, subnetPackageTypeMap[typeStr])
if err != nil {
return &datatypes.Container_Product_Order_Network_Subnet{}, err
}

// 2. Get all prices for the package
productItems, err := product.GetPackageProducts(sess, *pkg.Id)
if err != nil {
return &datatypes.Container_Product_Order_Network_Subnet{}, err
}

// 3. Get vlanType
vlanId := d.Get("vlan_id").(int)
vlanType, err := getVlanType(sess, vlanId)
if err != nil {
return &datatypes.Container_Product_Order_Network_Subnet{}, err
}

// 4. Select items which have a matching capacity, vlanType, and IP version.
capacity := d.Get("capacity").(int)
ipVersionStr := "_IP_"
if d.Get("ip_version").(int) == 6 {
ipVersionStr = "_IPV6_"
}
SubnetItems := []datatypes.Product_Item{}
for _, item := range productItems {
if int(*item.Capacity) == d.Get("capacity").(int) &&
strings.Contains(*item.KeyName, vlanType) &&
strings.Contains(*item.KeyName, ipVersionStr) {
SubnetItems = append(SubnetItems, item)
}
}

if len(SubnetItems) == 0 {
return &datatypes.Container_Product_Order_Network_Subnet{},
fmt.Errorf("No product items matching with capacity %d could be found", capacity)
}

productOrderContainer := datatypes.Container_Product_Order_Network_Subnet{
Container_Product_Order: datatypes.Container_Product_Order{
PackageId: pkg.Id,
Prices: []datatypes.Product_Item_Price{
{
Id: SubnetItems[0].Prices[0].Id,
},
},
Quantity: sl.Int(1),
},
EndPointVlanId: sl.Int(vlanId),
}

if endpointIp, ok := d.GetOk("endpoint_ip"); ok {
if typeStr != "STATIC_IP_ROUTED" {
return &datatypes.Container_Product_Order_Network_Subnet{},
fmt.Errorf("endpoint_ip is only available when type is STATIC_IP_ROUTED.")
}
endpointIpStr := endpointIp.(string)
subnet, err := services.GetNetworkSubnetService(sess).Mask("ipAddresses").GetSubnetForIpAddress(sl.String(endpointIpStr))
if err != nil {
return &datatypes.Container_Product_Order_Network_Subnet{}, err
}
for _, ipSubnet := range subnet.IpAddresses {
if *ipSubnet.IpAddress == endpointIpStr {
productOrderContainer.EndPointIpAddressId = ipSubnet.Id
}
}
if productOrderContainer.EndPointIpAddressId == nil {
return &datatypes.Container_Product_Order_Network_Subnet{},
fmt.Errorf("Unable to find an ID of ipAddress: %s", endpointIpStr)
}
}
return &productOrderContainer, nil
}

func getVlanType(sess *session.Session, vlanId int) (string, error) {
vlan, err := services.GetNetworkVlanService(sess).Id(vlanId).Mask(VlanMask).GetObject()

if err != nil {
return "", fmt.Errorf("Error retrieving vlan: %s", err)
}

if vlan.PrimaryRouter != nil {
if strings.HasPrefix(*vlan.PrimaryRouter.Hostname, "fcr") {
return "PUBLIC", nil
} else {
return "PRIVATE", nil
}
}
return "", fmt.Errorf("Unable to determine vlan_type.")
}