Merge pull request #4893 from ack/clc
provider/clc: CenturyLink Cloud Provider
This commit is contained in:
commit
b2d3f92f7b
|
@ -150,6 +150,51 @@
|
||||||
"Comment": "v1.2-315-g1cb9dff",
|
"Comment": "v1.2-315-g1cb9dff",
|
||||||
"Rev": "1cb9dff8c37b2918ad1ebd7b294d01100a153d27"
|
"Rev": "1cb9dff8c37b2918ad1ebd7b294d01100a153d27"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"ImportPath": "github.com/CenturyLinkCloud/clc-sdk",
|
||||||
|
"Comment": "0.0.2-19-gbe16cca",
|
||||||
|
"Rev": "be16cca3fa780c77916e52370a9c89cc53bcf73a"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ImportPath": "github.com/CenturyLinkCloud/clc-sdk/aa",
|
||||||
|
"Comment": "0.0.2-19-gbe16cca",
|
||||||
|
"Rev": "be16cca3fa780c77916e52370a9c89cc53bcf73a"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ImportPath": "github.com/CenturyLinkCloud/clc-sdk/alert",
|
||||||
|
"Comment": "0.0.2-19-gbe16cca",
|
||||||
|
"Rev": "be16cca3fa780c77916e52370a9c89cc53bcf73a"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ImportPath": "github.com/CenturyLinkCloud/clc-sdk/api",
|
||||||
|
"Comment": "0.0.2-19-gbe16cca",
|
||||||
|
"Rev": "be16cca3fa780c77916e52370a9c89cc53bcf73a"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ImportPath": "github.com/CenturyLinkCloud/clc-sdk/dc",
|
||||||
|
"Comment": "0.0.2-19-gbe16cca",
|
||||||
|
"Rev": "be16cca3fa780c77916e52370a9c89cc53bcf73a"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ImportPath": "github.com/CenturyLinkCloud/clc-sdk/group",
|
||||||
|
"Comment": "0.0.2-19-gbe16cca",
|
||||||
|
"Rev": "be16cca3fa780c77916e52370a9c89cc53bcf73a"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ImportPath": "github.com/CenturyLinkCloud/clc-sdk/lb",
|
||||||
|
"Comment": "0.0.2-19-gbe16cca",
|
||||||
|
"Rev": "be16cca3fa780c77916e52370a9c89cc53bcf73a"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ImportPath": "github.com/CenturyLinkCloud/clc-sdk/server",
|
||||||
|
"Comment": "0.0.2-19-gbe16cca",
|
||||||
|
"Rev": "be16cca3fa780c77916e52370a9c89cc53bcf73a"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ImportPath": "github.com/CenturyLinkCloud/clc-sdk/status",
|
||||||
|
"Comment": "0.0.2-19-gbe16cca",
|
||||||
|
"Rev": "be16cca3fa780c77916e52370a9c89cc53bcf73a"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"ImportPath": "github.com/DreamItGetIT/statuscake",
|
"ImportPath": "github.com/DreamItGetIT/statuscake",
|
||||||
"Rev": "8cbe86575f00210a6df2c19cb2f59b00cd181de3"
|
"Rev": "8cbe86575f00210a6df2c19cb2f59b00cd181de3"
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/hashicorp/terraform/builtin/providers/clc"
|
||||||
|
"github.com/hashicorp/terraform/plugin"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
plugin.Serve(&plugin.ServeOpts{
|
||||||
|
ProviderFunc: clc.Provider,
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
package main
|
|
@ -0,0 +1,195 @@
|
||||||
|
package clc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
clc "github.com/CenturyLinkCloud/clc-sdk"
|
||||||
|
"github.com/CenturyLinkCloud/clc-sdk/api"
|
||||||
|
"github.com/CenturyLinkCloud/clc-sdk/group"
|
||||||
|
"github.com/CenturyLinkCloud/clc-sdk/server"
|
||||||
|
"github.com/CenturyLinkCloud/clc-sdk/status"
|
||||||
|
"github.com/hashicorp/terraform/helper/schema"
|
||||||
|
"github.com/hashicorp/terraform/terraform"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Provider implements ResourceProvider for CLC
|
||||||
|
func Provider() terraform.ResourceProvider {
|
||||||
|
return &schema.Provider{
|
||||||
|
Schema: map[string]*schema.Schema{
|
||||||
|
"username": &schema.Schema{
|
||||||
|
Type: schema.TypeString,
|
||||||
|
Required: true,
|
||||||
|
DefaultFunc: schema.EnvDefaultFunc("CLC_USERNAME", nil),
|
||||||
|
Description: "Your CLC username",
|
||||||
|
},
|
||||||
|
"password": &schema.Schema{
|
||||||
|
Type: schema.TypeString,
|
||||||
|
Required: true,
|
||||||
|
DefaultFunc: schema.EnvDefaultFunc("CLC_PASSWORD", nil),
|
||||||
|
Description: "Your CLC password",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
ResourcesMap: map[string]*schema.Resource{
|
||||||
|
"clc_server": resourceCLCServer(),
|
||||||
|
"clc_group": resourceCLCGroup(),
|
||||||
|
"clc_public_ip": resourceCLCPublicIP(),
|
||||||
|
"clc_load_balancer": resourceCLCLoadBalancer(),
|
||||||
|
"clc_load_balancer_pool": resourceCLCLoadBalancerPool(),
|
||||||
|
},
|
||||||
|
|
||||||
|
ConfigureFunc: providerConfigure,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func providerConfigure(d *schema.ResourceData) (interface{}, error) {
|
||||||
|
un := d.Get("username").(string)
|
||||||
|
pw := d.Get("password").(string)
|
||||||
|
|
||||||
|
config, err := api.NewConfig(un, pw)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("Failed to create CLC config with provided details: %v", err)
|
||||||
|
}
|
||||||
|
config.UserAgent = fmt.Sprintf("terraform-clc terraform/%s", terraform.Version)
|
||||||
|
|
||||||
|
client := clc.New(config)
|
||||||
|
if err := client.Authenticate(); err != nil {
|
||||||
|
return nil, fmt.Errorf("Failed authenticated with provided credentials: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
alerts, err := client.Alert.GetAll()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("Failed to connect to the CLC api because %s", err)
|
||||||
|
}
|
||||||
|
for _, a := range alerts.Items {
|
||||||
|
log.Printf("[WARN] Received alert: %v", a)
|
||||||
|
}
|
||||||
|
return client, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// package utility functions
|
||||||
|
|
||||||
|
func waitStatus(client *clc.Client, id string) error {
|
||||||
|
// block until queue is processed and server is up
|
||||||
|
poll := make(chan *status.Response, 1)
|
||||||
|
err := client.Status.Poll(id, poll)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
status := <-poll
|
||||||
|
log.Printf("[DEBUG] status %v", status)
|
||||||
|
if status.Failed() {
|
||||||
|
return fmt.Errorf("unsuccessful job %v failed with status: %v", id, status.Status)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func dcGroups(dcname string, client *clc.Client) (map[string]string, error) {
|
||||||
|
dc, _ := client.DC.Get(dcname)
|
||||||
|
_, id := dc.Links.GetID("group")
|
||||||
|
m := map[string]string{}
|
||||||
|
resp, _ := client.Group.Get(id)
|
||||||
|
m[resp.Name] = resp.ID // top
|
||||||
|
m[resp.ID] = resp.ID
|
||||||
|
for _, x := range resp.Groups {
|
||||||
|
deepGroups(x, &m)
|
||||||
|
}
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func deepGroups(g group.Groups, m *map[string]string) {
|
||||||
|
(*m)[g.Name] = g.ID
|
||||||
|
(*m)[g.ID] = g.ID
|
||||||
|
for _, sg := range g.Groups {
|
||||||
|
deepGroups(sg, m)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// resolveGroupByNameOrId takes a reference to a group (either name or guid)
|
||||||
|
// and returns the guid of the group
|
||||||
|
func resolveGroupByNameOrId(ref, dc string, client *clc.Client) (string, error) {
|
||||||
|
m, err := dcGroups(dc, client)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("Failed pulling groups in location %v - %v", dc, err)
|
||||||
|
}
|
||||||
|
if id, ok := m[ref]; ok {
|
||||||
|
return id, nil
|
||||||
|
}
|
||||||
|
return "", fmt.Errorf("Failed resolving group '%v' in location %v", ref, dc)
|
||||||
|
}
|
||||||
|
|
||||||
|
func stateFromString(st string) server.PowerState {
|
||||||
|
switch st {
|
||||||
|
case "on", "started":
|
||||||
|
return server.On
|
||||||
|
case "off", "stopped":
|
||||||
|
return server.Off
|
||||||
|
case "pause", "paused":
|
||||||
|
return server.Pause
|
||||||
|
case "reboot":
|
||||||
|
return server.Reboot
|
||||||
|
case "reset":
|
||||||
|
return server.Reset
|
||||||
|
case "shutdown":
|
||||||
|
return server.ShutDown
|
||||||
|
case "start_maintenance":
|
||||||
|
return server.StartMaintenance
|
||||||
|
case "stop_maintenance":
|
||||||
|
return server.StopMaintenance
|
||||||
|
}
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseCustomFields(d *schema.ResourceData) ([]api.Customfields, error) {
|
||||||
|
var fields []api.Customfields
|
||||||
|
if v := d.Get("custom_fields"); v != nil {
|
||||||
|
for _, v := range v.([]interface{}) {
|
||||||
|
m := v.(map[string]interface{})
|
||||||
|
f := api.Customfields{
|
||||||
|
ID: m["id"].(string),
|
||||||
|
Value: m["value"].(string),
|
||||||
|
}
|
||||||
|
fields = append(fields, f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fields, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseAdditionalDisks(d *schema.ResourceData) ([]server.Disk, error) {
|
||||||
|
// some complexity here: create has a different format than update
|
||||||
|
// on-create: { path, sizeGB, type }
|
||||||
|
// on-update: { diskId, sizeGB, (path), (type=partitioned) }
|
||||||
|
var disks []server.Disk
|
||||||
|
if v := d.Get("additional_disks"); v != nil {
|
||||||
|
for _, v := range v.([]interface{}) {
|
||||||
|
m := v.(map[string]interface{})
|
||||||
|
ty := m["type"].(string)
|
||||||
|
var pa string
|
||||||
|
if nil != m["path"] {
|
||||||
|
pa = m["path"].(string)
|
||||||
|
}
|
||||||
|
sz, err := strconv.Atoi(m["size_gb"].(string))
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[WARN] Failed parsing size '%v'. skipping", m["size_gb"])
|
||||||
|
return nil, fmt.Errorf("Unable to parse %v as int", m["size_gb"])
|
||||||
|
}
|
||||||
|
if ty != "raw" && ty != "partitioned" {
|
||||||
|
return nil, fmt.Errorf("Expected type of { raw | partitioned }. received %v", ty)
|
||||||
|
}
|
||||||
|
if ty == "raw" && pa != "" {
|
||||||
|
return nil, fmt.Errorf("Path can not be specified for raw disks")
|
||||||
|
}
|
||||||
|
disk := server.Disk{
|
||||||
|
SizeGB: sz,
|
||||||
|
Type: ty,
|
||||||
|
}
|
||||||
|
if pa != "" {
|
||||||
|
disk.Path = pa
|
||||||
|
}
|
||||||
|
disks = append(disks, disk)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return disks, nil
|
||||||
|
}
|
|
@ -0,0 +1,40 @@
|
||||||
|
package clc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform/helper/schema"
|
||||||
|
"github.com/hashicorp/terraform/terraform"
|
||||||
|
)
|
||||||
|
|
||||||
|
var testAccProviders map[string]terraform.ResourceProvider
|
||||||
|
var testAccProvider *schema.Provider
|
||||||
|
|
||||||
|
const testAccDC = "IL1"
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
testAccProvider = Provider().(*schema.Provider)
|
||||||
|
testAccProviders = map[string]terraform.ResourceProvider{
|
||||||
|
"clc": 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("CLC_USERNAME"); v == "" {
|
||||||
|
t.Fatal("CLC_USERNAME must be set for acceptance tests")
|
||||||
|
}
|
||||||
|
if v := os.Getenv("CLC_PASSWORD"); v == "" {
|
||||||
|
t.Fatal("CLC_PASSWORD must be set for acceptance tests")
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,163 @@
|
||||||
|
package clc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/CenturyLinkCloud/clc-sdk"
|
||||||
|
"github.com/CenturyLinkCloud/clc-sdk/api"
|
||||||
|
"github.com/CenturyLinkCloud/clc-sdk/group"
|
||||||
|
"github.com/hashicorp/terraform/helper/resource"
|
||||||
|
"github.com/hashicorp/terraform/helper/schema"
|
||||||
|
)
|
||||||
|
|
||||||
|
func resourceCLCGroup() *schema.Resource {
|
||||||
|
return &schema.Resource{
|
||||||
|
Create: resourceCLCGroupCreate,
|
||||||
|
Read: resourceCLCGroupRead,
|
||||||
|
Update: resourceCLCGroupUpdate,
|
||||||
|
Delete: resourceCLCGroupDelete,
|
||||||
|
Schema: map[string]*schema.Schema{
|
||||||
|
"name": &schema.Schema{
|
||||||
|
Type: schema.TypeString,
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
"description": &schema.Schema{
|
||||||
|
Type: schema.TypeString,
|
||||||
|
Optional: true,
|
||||||
|
Default: "",
|
||||||
|
},
|
||||||
|
"parent": &schema.Schema{
|
||||||
|
Type: schema.TypeString,
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
"location_id": &schema.Schema{
|
||||||
|
Type: schema.TypeString,
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
"parent_group_id": &schema.Schema{
|
||||||
|
Type: schema.TypeString,
|
||||||
|
Computed: true,
|
||||||
|
},
|
||||||
|
"custom_fields": &schema.Schema{
|
||||||
|
Type: schema.TypeList,
|
||||||
|
Optional: true,
|
||||||
|
Elem: &schema.Schema{Type: schema.TypeMap},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func resourceCLCGroupCreate(d *schema.ResourceData, meta interface{}) error {
|
||||||
|
client := meta.(*clc.Client)
|
||||||
|
name := d.Get("name").(string)
|
||||||
|
desc := d.Get("description").(string)
|
||||||
|
parent := d.Get("parent").(string)
|
||||||
|
dc := d.Get("location_id").(string)
|
||||||
|
|
||||||
|
// clc doesn't enforce uniqueness by name
|
||||||
|
// so skip the trad'l error we'd raise
|
||||||
|
e, err := resolveGroupByNameOrId(name, dc, client)
|
||||||
|
if e != "" {
|
||||||
|
log.Printf("[INFO] Resolved existing group: %v => %v", name, e)
|
||||||
|
d.SetId(e)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var pgid string
|
||||||
|
p, err := resolveGroupByNameOrId(parent, dc, client)
|
||||||
|
if p != "" {
|
||||||
|
log.Printf("[INFO] Resolved parent group: %v => %v", parent, p)
|
||||||
|
pgid = p
|
||||||
|
} else {
|
||||||
|
return fmt.Errorf("Failed resolving parent group %s - %s err:%s", parent, p, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
d.Set("parent_group_id", pgid)
|
||||||
|
spec := group.Group{
|
||||||
|
Name: name,
|
||||||
|
Description: desc,
|
||||||
|
ParentGroupID: pgid,
|
||||||
|
}
|
||||||
|
resp, err := client.Group.Create(spec)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Failed creating group: %s", err)
|
||||||
|
}
|
||||||
|
log.Println("[INFO] Group created")
|
||||||
|
d.SetId(resp.ID)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func resourceCLCGroupRead(d *schema.ResourceData, meta interface{}) error {
|
||||||
|
client := meta.(*clc.Client)
|
||||||
|
id := d.Id()
|
||||||
|
g, err := client.Group.Get(id)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[INFO] Failed finding group: %s - %s. Marking destroyed", id, err)
|
||||||
|
d.SetId("")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
d.Set("name", g.Name)
|
||||||
|
d.Set("description", g.Description)
|
||||||
|
d.Set("parent_group_id", g.ParentGroupID())
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func resourceCLCGroupUpdate(d *schema.ResourceData, meta interface{}) error {
|
||||||
|
client := meta.(*clc.Client)
|
||||||
|
id := d.Id()
|
||||||
|
var err error
|
||||||
|
var patches []api.Update
|
||||||
|
|
||||||
|
g, err := client.Group.Get(id)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Failed fetching group: %v - %v", id, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if delta, orig := d.Get("name").(string), g.Name; delta != orig {
|
||||||
|
patches = append(patches, group.UpdateName(delta))
|
||||||
|
}
|
||||||
|
if delta, orig := d.Get("description").(string), g.Description; delta != orig {
|
||||||
|
patches = append(patches, group.UpdateDescription(delta))
|
||||||
|
}
|
||||||
|
newParent := d.Get("parent").(string)
|
||||||
|
pgid, err := resolveGroupByNameOrId(newParent, g.Locationid, client)
|
||||||
|
log.Printf("[DEBUG] PARENT current:%v new:%v resolved:%v", g.ParentGroupID(), newParent, pgid)
|
||||||
|
if pgid == "" {
|
||||||
|
return fmt.Errorf("Unable to resolve parent group %v: %v", newParent, err)
|
||||||
|
} else if newParent != g.ParentGroupID() {
|
||||||
|
patches = append(patches, group.UpdateParentGroupID(pgid))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(patches) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
err = client.Group.Update(id, patches...)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Failed updating group %v: %v", id, err)
|
||||||
|
}
|
||||||
|
return resource.Retry(1*time.Minute, func() *resource.RetryError {
|
||||||
|
_, err := client.Group.Get(id)
|
||||||
|
if err != nil {
|
||||||
|
return resource.RetryableError(err)
|
||||||
|
}
|
||||||
|
err = resourceCLCGroupRead(d, meta)
|
||||||
|
if err != nil {
|
||||||
|
return resource.NonRetryableError(err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func resourceCLCGroupDelete(d *schema.ResourceData, meta interface{}) error {
|
||||||
|
client := meta.(*clc.Client)
|
||||||
|
id := d.Id()
|
||||||
|
log.Printf("[INFO] Deleting group %v", id)
|
||||||
|
st, err := client.Group.Delete(id)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Failed deleting group: %v with err: %v", id, err)
|
||||||
|
}
|
||||||
|
waitStatus(client, st.ID)
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,155 @@
|
||||||
|
package clc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
clc "github.com/CenturyLinkCloud/clc-sdk"
|
||||||
|
"github.com/CenturyLinkCloud/clc-sdk/group"
|
||||||
|
"github.com/hashicorp/terraform/helper/resource"
|
||||||
|
"github.com/hashicorp/terraform/terraform"
|
||||||
|
)
|
||||||
|
|
||||||
|
// things to test:
|
||||||
|
// resolves to existing group
|
||||||
|
// does not nuke a group w/ no parents (root group)
|
||||||
|
// change a name on a group
|
||||||
|
|
||||||
|
func TestAccGroupBasic(t *testing.T) {
|
||||||
|
var resp group.Response
|
||||||
|
resource.Test(t, resource.TestCase{
|
||||||
|
PreCheck: func() { testAccPreCheck(t) },
|
||||||
|
Providers: testAccProviders,
|
||||||
|
CheckDestroy: testAccCheckGroupDestroy,
|
||||||
|
Steps: []resource.TestStep{
|
||||||
|
resource.TestStep{
|
||||||
|
Config: testAccCheckGroupConfigBasic,
|
||||||
|
Check: resource.ComposeTestCheckFunc(
|
||||||
|
testAccCheckGroupExists("clc_group.acc_test_group", &resp),
|
||||||
|
testAccCheckGroupParent(&resp, "Default Group"),
|
||||||
|
resource.TestCheckResourceAttr(
|
||||||
|
"clc_group.acc_test_group", "name", "okcomputer"),
|
||||||
|
resource.TestCheckResourceAttr(
|
||||||
|
"clc_group.acc_test_group", "location_id", testAccDC),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
resource.TestStep{
|
||||||
|
Config: testAccCheckGroupConfigUpdate,
|
||||||
|
Check: resource.ComposeTestCheckFunc(
|
||||||
|
testAccCheckGroupExists("clc_group.acc_test_group", &resp),
|
||||||
|
testAccCheckGroupParent(&resp, "Default Group"),
|
||||||
|
resource.TestCheckResourceAttr(
|
||||||
|
"clc_group.acc_test_group", "name", "foobar"),
|
||||||
|
resource.TestCheckResourceAttr(
|
||||||
|
"clc_group.acc_test_group", "location_id", testAccDC),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
resource.TestStep{
|
||||||
|
Config: testAccCheckGroupConfigReparent,
|
||||||
|
Check: resource.ComposeTestCheckFunc(
|
||||||
|
testAccCheckGroupExists("clc_group.acc_test_group", &resp),
|
||||||
|
testAccCheckGroupParent(&resp, "reparent"),
|
||||||
|
resource.TestCheckResourceAttr(
|
||||||
|
"clc_group.acc_test_group", "name", "foobar"),
|
||||||
|
resource.TestCheckResourceAttr(
|
||||||
|
"clc_group.acc_test_group", "location_id", testAccDC),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func testAccCheckGroupDestroy(s *terraform.State) error {
|
||||||
|
client := testAccProvider.Meta().(*clc.Client)
|
||||||
|
for _, rs := range s.RootModule().Resources {
|
||||||
|
if rs.Type != "clc_group" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
_, err := client.Group.Get(rs.Primary.ID)
|
||||||
|
if err == nil {
|
||||||
|
return fmt.Errorf("Group still exists")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func testAccCheckGroupParent(resp *group.Response, expectedName string) resource.TestCheckFunc {
|
||||||
|
return func(s *terraform.State) error {
|
||||||
|
client := testAccProvider.Meta().(*clc.Client)
|
||||||
|
ok, l := resp.Links.GetLink("parentGroup")
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("Missing parent group: %v", resp)
|
||||||
|
}
|
||||||
|
parent, err := client.Group.Get(l.ID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Failed fetching parent %v: %v", l.ID, err)
|
||||||
|
}
|
||||||
|
if parent.Name != expectedName {
|
||||||
|
return fmt.Errorf("Incorrect parent found:'%v' expected:'%v'", parent.Name, expectedName)
|
||||||
|
}
|
||||||
|
// would be good to test parent but we'd have to make a bunch of calls
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testAccCheckGroupExists(n string, resp *group.Response) 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 Group ID is set")
|
||||||
|
}
|
||||||
|
|
||||||
|
client := testAccProvider.Meta().(*clc.Client)
|
||||||
|
g, err := client.Group.Get(rs.Primary.ID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if g.ID != rs.Primary.ID {
|
||||||
|
return fmt.Errorf("Group not found")
|
||||||
|
}
|
||||||
|
*resp = *g
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const testAccCheckGroupConfigBasic = `
|
||||||
|
variable "dc" { default = "IL1" }
|
||||||
|
|
||||||
|
resource "clc_group" "acc_test_group" {
|
||||||
|
location_id = "${var.dc}"
|
||||||
|
name = "okcomputer"
|
||||||
|
description = "mishaps happening"
|
||||||
|
parent = "Default Group"
|
||||||
|
}`
|
||||||
|
|
||||||
|
const testAccCheckGroupConfigUpdate = `
|
||||||
|
variable "dc" { default = "IL1" }
|
||||||
|
|
||||||
|
resource "clc_group" "acc_test_group" {
|
||||||
|
location_id = "${var.dc}"
|
||||||
|
name = "foobar"
|
||||||
|
description = "update test"
|
||||||
|
parent = "Default Group"
|
||||||
|
}`
|
||||||
|
|
||||||
|
const testAccCheckGroupConfigReparent = `
|
||||||
|
variable "dc" { default = "IL1" }
|
||||||
|
|
||||||
|
resource "clc_group" "acc_test_group_reparent" {
|
||||||
|
location_id = "${var.dc}"
|
||||||
|
name = "reparent"
|
||||||
|
description = "introduce a parent group in place"
|
||||||
|
parent = "Default Group"
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "clc_group" "acc_test_group" {
|
||||||
|
location_id = "${var.dc}"
|
||||||
|
name = "foobar"
|
||||||
|
description = "update test"
|
||||||
|
parent = "${clc_group.acc_test_group_reparent.id}"
|
||||||
|
}
|
||||||
|
`
|
|
@ -0,0 +1,129 @@
|
||||||
|
package clc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
clc "github.com/CenturyLinkCloud/clc-sdk"
|
||||||
|
"github.com/CenturyLinkCloud/clc-sdk/lb"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform/helper/resource"
|
||||||
|
"github.com/hashicorp/terraform/helper/schema"
|
||||||
|
)
|
||||||
|
|
||||||
|
func resourceCLCLoadBalancer() *schema.Resource {
|
||||||
|
return &schema.Resource{
|
||||||
|
Create: resourceCLCLoadBalancerCreate,
|
||||||
|
Read: resourceCLCLoadBalancerRead,
|
||||||
|
Update: resourceCLCLoadBalancerUpdate,
|
||||||
|
Delete: resourceCLCLoadBalancerDelete,
|
||||||
|
Schema: map[string]*schema.Schema{
|
||||||
|
"name": &schema.Schema{
|
||||||
|
Type: schema.TypeString,
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
"data_center": &schema.Schema{
|
||||||
|
Type: schema.TypeString,
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
"description": &schema.Schema{
|
||||||
|
Type: schema.TypeString,
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
// optional
|
||||||
|
"status": &schema.Schema{
|
||||||
|
Type: schema.TypeString,
|
||||||
|
Optional: true,
|
||||||
|
Default: "enabled",
|
||||||
|
},
|
||||||
|
// computed
|
||||||
|
"ip_address": &schema.Schema{
|
||||||
|
Type: schema.TypeString,
|
||||||
|
Computed: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func resourceCLCLoadBalancerCreate(d *schema.ResourceData, meta interface{}) error {
|
||||||
|
client := meta.(*clc.Client)
|
||||||
|
dc := d.Get("data_center").(string)
|
||||||
|
name := d.Get("name").(string)
|
||||||
|
desc := d.Get("description").(string)
|
||||||
|
status := d.Get("status").(string)
|
||||||
|
r1 := lb.LoadBalancer{
|
||||||
|
Name: name,
|
||||||
|
Description: desc,
|
||||||
|
Status: status,
|
||||||
|
}
|
||||||
|
l, err := client.LB.Create(dc, r1)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Failed creating load balancer under %v/%v: %v", dc, name, err)
|
||||||
|
}
|
||||||
|
d.SetId(l.ID)
|
||||||
|
return resource.Retry(1*time.Minute, func() *resource.RetryError {
|
||||||
|
_, err := client.LB.Get(dc, l.ID)
|
||||||
|
if err != nil {
|
||||||
|
return resource.RetryableError(err)
|
||||||
|
}
|
||||||
|
err = resourceCLCLoadBalancerRead(d, meta)
|
||||||
|
if err != nil {
|
||||||
|
return resource.NonRetryableError(err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func resourceCLCLoadBalancerRead(d *schema.ResourceData, meta interface{}) error {
|
||||||
|
client := meta.(*clc.Client)
|
||||||
|
dc := d.Get("data_center").(string)
|
||||||
|
id := d.Id()
|
||||||
|
resp, err := client.LB.Get(dc, id)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[INFO] Failed finding load balancer %v/%v. Marking destroyed", dc, id)
|
||||||
|
d.SetId("")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
d.Set("description", resp.Description)
|
||||||
|
d.Set("ip_address", resp.IPaddress)
|
||||||
|
d.Set("status", resp.Status)
|
||||||
|
d.Set("pools", resp.Pools)
|
||||||
|
d.Set("links", resp.Links)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func resourceCLCLoadBalancerUpdate(d *schema.ResourceData, meta interface{}) error {
|
||||||
|
update := lb.LoadBalancer{}
|
||||||
|
client := meta.(*clc.Client)
|
||||||
|
dc := d.Get("data_center").(string)
|
||||||
|
id := d.Id()
|
||||||
|
|
||||||
|
if d.HasChange("name") {
|
||||||
|
update.Name = d.Get("name").(string)
|
||||||
|
}
|
||||||
|
if d.HasChange("description") {
|
||||||
|
update.Description = d.Get("description").(string)
|
||||||
|
}
|
||||||
|
if d.HasChange("status") {
|
||||||
|
update.Status = d.Get("status").(string)
|
||||||
|
}
|
||||||
|
if update.Name != "" || update.Description != "" || update.Status != "" {
|
||||||
|
err := client.LB.Update(dc, id, update)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Failed updating load balancer under %v/%v: %v", dc, id, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return resourceCLCLoadBalancerRead(d, meta)
|
||||||
|
}
|
||||||
|
|
||||||
|
func resourceCLCLoadBalancerDelete(d *schema.ResourceData, meta interface{}) error {
|
||||||
|
client := meta.(*clc.Client)
|
||||||
|
dc := d.Get("data_center").(string)
|
||||||
|
id := d.Id()
|
||||||
|
err := client.LB.Delete(dc, id)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Failed deleting loadbalancer %v: %v", id, err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,179 @@
|
||||||
|
package clc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
clc "github.com/CenturyLinkCloud/clc-sdk"
|
||||||
|
"github.com/CenturyLinkCloud/clc-sdk/lb"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform/helper/schema"
|
||||||
|
)
|
||||||
|
|
||||||
|
func resourceCLCLoadBalancerPool() *schema.Resource {
|
||||||
|
return &schema.Resource{
|
||||||
|
Create: resourceCLCLoadBalancerPoolCreate,
|
||||||
|
Read: resourceCLCLoadBalancerPoolRead,
|
||||||
|
Update: resourceCLCLoadBalancerPoolUpdate,
|
||||||
|
Delete: resourceCLCLoadBalancerPoolDelete,
|
||||||
|
Schema: map[string]*schema.Schema{
|
||||||
|
// pool args
|
||||||
|
"port": &schema.Schema{
|
||||||
|
Type: schema.TypeInt,
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
"data_center": &schema.Schema{
|
||||||
|
Type: schema.TypeString,
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
"load_balancer": &schema.Schema{
|
||||||
|
Type: schema.TypeString,
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
"method": &schema.Schema{
|
||||||
|
Type: schema.TypeString,
|
||||||
|
Optional: true,
|
||||||
|
Default: "roundRobin",
|
||||||
|
},
|
||||||
|
"persistence": &schema.Schema{
|
||||||
|
Type: schema.TypeString,
|
||||||
|
Optional: true,
|
||||||
|
Default: "standard",
|
||||||
|
},
|
||||||
|
"nodes": &schema.Schema{
|
||||||
|
Type: schema.TypeList,
|
||||||
|
Required: true,
|
||||||
|
Elem: &schema.Schema{Type: schema.TypeMap},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func resourceCLCLoadBalancerPoolCreate(d *schema.ResourceData, meta interface{}) error {
|
||||||
|
client := meta.(*clc.Client)
|
||||||
|
dc := d.Get("data_center").(string)
|
||||||
|
lbid := d.Get("load_balancer").(string)
|
||||||
|
|
||||||
|
s1 := d.Get("method").(string)
|
||||||
|
m := lb.LeastConn
|
||||||
|
if s1 == string(lb.RoundRobin) {
|
||||||
|
m = lb.RoundRobin
|
||||||
|
}
|
||||||
|
s2 := d.Get("persistence").(string)
|
||||||
|
p := lb.Standard
|
||||||
|
if s2 == string(lb.Sticky) {
|
||||||
|
p = lb.Sticky
|
||||||
|
}
|
||||||
|
r2 := lb.Pool{
|
||||||
|
Port: d.Get("port").(int),
|
||||||
|
Method: m,
|
||||||
|
Persistence: p,
|
||||||
|
}
|
||||||
|
lbp, err := client.LB.CreatePool(dc, lbid, r2)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Failed creating pool under %v/%v: %v", dc, lbid, err)
|
||||||
|
}
|
||||||
|
d.SetId(lbp.ID)
|
||||||
|
return resourceCLCLoadBalancerPoolUpdate(d, meta)
|
||||||
|
}
|
||||||
|
|
||||||
|
func resourceCLCLoadBalancerPoolRead(d *schema.ResourceData, meta interface{}) error {
|
||||||
|
client := meta.(*clc.Client)
|
||||||
|
dc := d.Get("data_center").(string)
|
||||||
|
lbid := d.Get("load_balancer").(string)
|
||||||
|
id := d.Id()
|
||||||
|
pool, err := client.LB.GetPool(dc, lbid, id)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[INFO] Failed fetching pool %v/%v. Marking destroyed", lbid, d.Id())
|
||||||
|
d.SetId("")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
nodes, err := client.LB.GetAllNodes(dc, lbid, id)
|
||||||
|
nodes2 := make([]lb.Node, len(nodes))
|
||||||
|
for i, n := range nodes {
|
||||||
|
nodes2[i] = *n
|
||||||
|
}
|
||||||
|
pool.Nodes = nodes2
|
||||||
|
d.Set("port", pool.Port)
|
||||||
|
d.Set("method", pool.Method)
|
||||||
|
d.Set("persistence", pool.Persistence)
|
||||||
|
d.Set("nodes", pool.Nodes)
|
||||||
|
d.Set("links", pool.Links)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func resourceCLCLoadBalancerPoolUpdate(d *schema.ResourceData, meta interface{}) error {
|
||||||
|
client := meta.(*clc.Client)
|
||||||
|
dc := d.Get("data_center").(string)
|
||||||
|
lbid := d.Get("load_balancer").(string)
|
||||||
|
id := d.Id()
|
||||||
|
pool, err := client.LB.GetPool(dc, lbid, d.Id())
|
||||||
|
pool.Port = 0 // triggers empty value => omission from POST
|
||||||
|
|
||||||
|
if d.HasChange("method") {
|
||||||
|
d.SetPartial("method")
|
||||||
|
pool.Method = lb.Method(d.Get("method").(string))
|
||||||
|
}
|
||||||
|
if d.HasChange("persistence") {
|
||||||
|
d.SetPartial("persistence")
|
||||||
|
pool.Persistence = lb.Persistence(d.Get("persistence").(string))
|
||||||
|
}
|
||||||
|
err = client.LB.UpdatePool(dc, lbid, id, *pool)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Failed updating pool %v: %v", id, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if d.HasChange("nodes") {
|
||||||
|
d.SetPartial("nodes")
|
||||||
|
nodes, err := parseNodes(d)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = client.LB.UpdateNodes(dc, lbid, id, nodes...)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Failed updating pool nodes %v: %v", id, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return resourceCLCLoadBalancerPoolRead(d, meta)
|
||||||
|
}
|
||||||
|
|
||||||
|
func resourceCLCLoadBalancerPoolDelete(d *schema.ResourceData, meta interface{}) error {
|
||||||
|
client := meta.(*clc.Client)
|
||||||
|
dc := d.Get("data_center").(string)
|
||||||
|
lbid := d.Get("load_balancer").(string)
|
||||||
|
id := d.Id()
|
||||||
|
err := client.LB.DeletePool(dc, lbid, id)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Failed deleting pool %v: %v", id, err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseNodes(d *schema.ResourceData) ([]lb.Node, error) {
|
||||||
|
var nodes []lb.Node
|
||||||
|
raw := d.Get("nodes")
|
||||||
|
if raw == nil {
|
||||||
|
log.Println("WARNING: pool missing nodes")
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
if arr, ok := raw.([]interface{}); ok {
|
||||||
|
for _, v := range arr {
|
||||||
|
m := v.(map[string]interface{})
|
||||||
|
p, err := strconv.Atoi(m["privatePort"].(string))
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[WARN] Failed parsing port '%v'. skipping", m["privatePort"])
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
n := lb.Node{
|
||||||
|
Status: m["status"].(string),
|
||||||
|
IPaddress: m["ipAddress"].(string),
|
||||||
|
PrivatePort: p,
|
||||||
|
}
|
||||||
|
nodes = append(nodes, n)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return nil, fmt.Errorf("Failed parsing nodes from pool spec: %v", raw)
|
||||||
|
}
|
||||||
|
return nodes, nil
|
||||||
|
}
|
|
@ -0,0 +1,117 @@
|
||||||
|
package clc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
clc "github.com/CenturyLinkCloud/clc-sdk"
|
||||||
|
lb "github.com/CenturyLinkCloud/clc-sdk/lb"
|
||||||
|
"github.com/hashicorp/terraform/helper/resource"
|
||||||
|
"github.com/hashicorp/terraform/terraform"
|
||||||
|
)
|
||||||
|
|
||||||
|
// things to test:
|
||||||
|
// basic create/delete
|
||||||
|
// update nodes
|
||||||
|
// works for 80 and 443 together
|
||||||
|
|
||||||
|
func TestAccLBPoolBasic(t *testing.T) {
|
||||||
|
var pool lb.Pool
|
||||||
|
resource.Test(t, resource.TestCase{
|
||||||
|
PreCheck: func() { testAccPreCheck(t) },
|
||||||
|
Providers: testAccProviders,
|
||||||
|
CheckDestroy: testAccCheckLBPDestroy,
|
||||||
|
Steps: []resource.TestStep{
|
||||||
|
resource.TestStep{
|
||||||
|
Config: testAccCheckLBPConfigBasic,
|
||||||
|
Check: resource.ComposeTestCheckFunc(
|
||||||
|
testAccCheckLBPExists("clc_load_balancer_pool.acc_test_pool", &pool),
|
||||||
|
resource.TestCheckResourceAttr("clc_load_balancer_pool.acc_test_pool", "port", "80"),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func testAccCheckLBPDestroy(s *terraform.State) error {
|
||||||
|
client := testAccProvider.Meta().(*clc.Client)
|
||||||
|
for _, rs := range s.RootModule().Resources {
|
||||||
|
if rs.Type != "clc_load_balancer_pool" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
lbid := rs.Primary.Attributes["load_balancer"]
|
||||||
|
if _, err := client.LB.Get(testAccDC, rs.Primary.ID); err != nil {
|
||||||
|
return nil // parent LB already gone
|
||||||
|
}
|
||||||
|
if _, err := client.LB.GetPool(testAccDC, lbid, rs.Primary.ID); err == nil {
|
||||||
|
return fmt.Errorf("LB still exists")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func testAccCheckLBPExists(n string, resp *lb.Pool) 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")
|
||||||
|
}
|
||||||
|
lbid := rs.Primary.Attributes["load_balancer"]
|
||||||
|
client := testAccProvider.Meta().(*clc.Client)
|
||||||
|
p, err := client.LB.GetPool(testAccDC, lbid, rs.Primary.ID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if p.ID != rs.Primary.ID {
|
||||||
|
return fmt.Errorf("Pool not found")
|
||||||
|
}
|
||||||
|
*resp = *p
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const testAccCheckLBPConfigBasic = `
|
||||||
|
variable "dc" { default = "IL1" }
|
||||||
|
|
||||||
|
resource "clc_group" "acc_test_lbp_group" {
|
||||||
|
location_id = "${var.dc}"
|
||||||
|
name = "acc_test_lbp_group"
|
||||||
|
parent = "Default Group"
|
||||||
|
}
|
||||||
|
|
||||||
|
# need a server here because we need to reference an ip owned by this account
|
||||||
|
resource "clc_server" "acc_test_lbp_server" {
|
||||||
|
name_template = "node"
|
||||||
|
description = "load balanced in ${clc_load_balancer.acc_test_lbp.id}"
|
||||||
|
source_server_id = "UBUNTU-14-64-TEMPLATE"
|
||||||
|
type = "standard"
|
||||||
|
group_id = "${clc_group.acc_test_lbp_group.id}"
|
||||||
|
cpu = 1
|
||||||
|
memory_mb = 1024
|
||||||
|
password = "Green123$"
|
||||||
|
power_state = "started"
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "clc_load_balancer" "acc_test_lbp" {
|
||||||
|
data_center = "${var.dc}"
|
||||||
|
name = "acc_test_lb"
|
||||||
|
description = "load balancer test"
|
||||||
|
status = "enabled"
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "clc_load_balancer_pool" "acc_test_pool" {
|
||||||
|
port = 80
|
||||||
|
data_center = "${var.dc}"
|
||||||
|
load_balancer = "${clc_load_balancer.acc_test_lbp.id}"
|
||||||
|
nodes
|
||||||
|
{
|
||||||
|
status = "enabled"
|
||||||
|
ipAddress = "${clc_server.acc_test_lbp_server.private_ip_address}"
|
||||||
|
privatePort = 80
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
|
@ -0,0 +1,101 @@
|
||||||
|
package clc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
clc "github.com/CenturyLinkCloud/clc-sdk"
|
||||||
|
lb "github.com/CenturyLinkCloud/clc-sdk/lb"
|
||||||
|
"github.com/hashicorp/terraform/helper/resource"
|
||||||
|
"github.com/hashicorp/terraform/terraform"
|
||||||
|
)
|
||||||
|
|
||||||
|
// things to test:
|
||||||
|
// updates name/desc
|
||||||
|
// toggles status
|
||||||
|
// created w/o pool
|
||||||
|
|
||||||
|
func TestAccLoadBalancerBasic(t *testing.T) {
|
||||||
|
var resp lb.LoadBalancer
|
||||||
|
resource.Test(t, resource.TestCase{
|
||||||
|
PreCheck: func() { testAccPreCheck(t) },
|
||||||
|
Providers: testAccProviders,
|
||||||
|
CheckDestroy: testAccCheckLBDestroy,
|
||||||
|
Steps: []resource.TestStep{
|
||||||
|
resource.TestStep{
|
||||||
|
Config: testAccCheckLBConfigBasic,
|
||||||
|
Check: resource.ComposeTestCheckFunc(
|
||||||
|
testAccCheckLBExists("clc_load_balancer.acc_test_lb", &resp),
|
||||||
|
resource.TestCheckResourceAttr("clc_load_balancer.acc_test_lb", "name", "acc_test_lb"),
|
||||||
|
resource.TestCheckResourceAttr("clc_load_balancer.acc_test_lb", "data_center", testAccDC),
|
||||||
|
resource.TestCheckResourceAttr("clc_load_balancer.acc_test_lb", "status", "enabled"),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
// update simple attrs
|
||||||
|
resource.TestStep{
|
||||||
|
Config: testAccCheckLBConfigNameDesc,
|
||||||
|
Check: resource.ComposeTestCheckFunc(
|
||||||
|
testAccCheckLBExists("clc_load_balancer.acc_test_lb", &resp),
|
||||||
|
resource.TestCheckResourceAttr("clc_load_balancer.acc_test_lb", "name", "foobar"),
|
||||||
|
resource.TestCheckResourceAttr("clc_load_balancer.acc_test_lb", "description", "foobar"),
|
||||||
|
resource.TestCheckResourceAttr("clc_load_balancer.acc_test_lb", "status", "disabled"),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func testAccCheckLBDestroy(s *terraform.State) error {
|
||||||
|
client := testAccProvider.Meta().(*clc.Client)
|
||||||
|
for _, rs := range s.RootModule().Resources {
|
||||||
|
if rs.Type != "clc_load_balancer" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, err := client.LB.Get(testAccDC, rs.Primary.ID); err == nil {
|
||||||
|
return fmt.Errorf("LB still exists")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func testAccCheckLBExists(n string, resp *lb.LoadBalancer) 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")
|
||||||
|
}
|
||||||
|
client := testAccProvider.Meta().(*clc.Client)
|
||||||
|
l, err := client.LB.Get(testAccDC, rs.Primary.ID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if l.ID != rs.Primary.ID {
|
||||||
|
return fmt.Errorf("LB not found")
|
||||||
|
}
|
||||||
|
*resp = *l
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const testAccCheckLBConfigBasic = `
|
||||||
|
variable "dc" { default = "IL1" }
|
||||||
|
|
||||||
|
resource "clc_load_balancer" "acc_test_lb" {
|
||||||
|
data_center = "${var.dc}"
|
||||||
|
name = "acc_test_lb"
|
||||||
|
description = "load balancer test"
|
||||||
|
status = "enabled"
|
||||||
|
}`
|
||||||
|
|
||||||
|
const testAccCheckLBConfigNameDesc = `
|
||||||
|
variable "dc" { default = "IL1" }
|
||||||
|
|
||||||
|
resource "clc_load_balancer" "acc_test_lb" {
|
||||||
|
data_center = "${var.dc}"
|
||||||
|
name = "foobar"
|
||||||
|
description = "foobar"
|
||||||
|
status = "disabled"
|
||||||
|
}`
|
|
@ -0,0 +1,193 @@
|
||||||
|
package clc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
clc "github.com/CenturyLinkCloud/clc-sdk"
|
||||||
|
"github.com/CenturyLinkCloud/clc-sdk/server"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform/helper/schema"
|
||||||
|
)
|
||||||
|
|
||||||
|
func resourceCLCPublicIP() *schema.Resource {
|
||||||
|
return &schema.Resource{
|
||||||
|
Create: resourceCLCPublicIPCreate,
|
||||||
|
Read: resourceCLCPublicIPRead,
|
||||||
|
Update: resourceCLCPublicIPUpdate,
|
||||||
|
Delete: resourceCLCPublicIPDelete,
|
||||||
|
Schema: map[string]*schema.Schema{
|
||||||
|
"server_id": &schema.Schema{
|
||||||
|
Type: schema.TypeString,
|
||||||
|
Required: true,
|
||||||
|
ForceNew: true,
|
||||||
|
},
|
||||||
|
"internal_ip_address": &schema.Schema{
|
||||||
|
Type: schema.TypeString,
|
||||||
|
Optional: true,
|
||||||
|
Computed: true,
|
||||||
|
Default: nil,
|
||||||
|
},
|
||||||
|
"ports": &schema.Schema{
|
||||||
|
Type: schema.TypeList,
|
||||||
|
Required: true,
|
||||||
|
Elem: &schema.Schema{Type: schema.TypeMap},
|
||||||
|
},
|
||||||
|
"source_restrictions": &schema.Schema{
|
||||||
|
Type: schema.TypeList,
|
||||||
|
Optional: true,
|
||||||
|
Elem: &schema.Schema{Type: schema.TypeMap},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func resourceCLCPublicIPCreate(d *schema.ResourceData, meta interface{}) error {
|
||||||
|
client := meta.(*clc.Client)
|
||||||
|
sid := d.Get("server_id").(string)
|
||||||
|
priv := d.Get("internal_ip_address").(string)
|
||||||
|
ports, sources := parseIPSpec(d)
|
||||||
|
req := server.PublicIP{
|
||||||
|
Ports: *ports,
|
||||||
|
SourceRestrictions: *sources,
|
||||||
|
}
|
||||||
|
|
||||||
|
// since the API doesn't tell us the public IP it allocated,
|
||||||
|
// track what was added after the call.
|
||||||
|
ips := make(map[string]string)
|
||||||
|
prev, err := client.Server.Get(sid)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Failed finding server %v: %v", sid, err)
|
||||||
|
}
|
||||||
|
for _, i := range prev.Details.IPaddresses {
|
||||||
|
ips[i.Internal] = i.Public
|
||||||
|
}
|
||||||
|
|
||||||
|
if priv != "" {
|
||||||
|
// use existing private ip
|
||||||
|
if _, present := ips[priv]; !present {
|
||||||
|
return fmt.Errorf("Failed finding internal ip to use %v", priv)
|
||||||
|
}
|
||||||
|
req.InternalIP = priv
|
||||||
|
}
|
||||||
|
// execute the request
|
||||||
|
resp, err := client.Server.AddPublicIP(sid, req)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Failed reserving public ip: %v", err)
|
||||||
|
}
|
||||||
|
err = waitStatus(client, resp.ID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
server, err := client.Server.Get(sid)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Failed refreshing server for public ip: %v", err)
|
||||||
|
}
|
||||||
|
for _, i := range server.Details.IPaddresses {
|
||||||
|
if priv != "" && i.Internal == priv {
|
||||||
|
// bind
|
||||||
|
log.Printf("[DEBUG] Public IP bound on existing internal:%v - %v", i.Internal, i.Public)
|
||||||
|
d.SetId(i.Public)
|
||||||
|
break
|
||||||
|
} else if ips[i.Internal] == "" && i.Public != "" {
|
||||||
|
// allocate
|
||||||
|
log.Printf("[DEBUG] Public IP allocated on new internal:%v - %v", i.Internal, i.Public)
|
||||||
|
d.SetId(i.Public)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return resourceCLCPublicIPRead(d, meta)
|
||||||
|
}
|
||||||
|
|
||||||
|
func resourceCLCPublicIPRead(d *schema.ResourceData, meta interface{}) error {
|
||||||
|
client := meta.(*clc.Client)
|
||||||
|
pip := d.Id()
|
||||||
|
s := d.Get("server_id").(string)
|
||||||
|
resp, err := client.Server.GetPublicIP(s, pip)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[INFO] Failed finding public ip: %v. Marking destroyed", d.Id())
|
||||||
|
d.SetId("")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
d.Set("internal_ip_address", resp.InternalIP)
|
||||||
|
d.Set("ports", resp.Ports)
|
||||||
|
d.Set("source_restrictions", resp.SourceRestrictions)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func resourceCLCPublicIPUpdate(d *schema.ResourceData, meta interface{}) error {
|
||||||
|
client := meta.(*clc.Client)
|
||||||
|
ip := d.Id()
|
||||||
|
sid := d.Get("server_id").(string)
|
||||||
|
if d.HasChange("ports") || d.HasChange("source_restrictions") {
|
||||||
|
ports, sources := parseIPSpec(d)
|
||||||
|
req := server.PublicIP{
|
||||||
|
Ports: *ports,
|
||||||
|
SourceRestrictions: *sources,
|
||||||
|
}
|
||||||
|
resp, err := client.Server.UpdatePublicIP(sid, ip, req)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Failed updating public ip: %v", err)
|
||||||
|
}
|
||||||
|
err = waitStatus(client, resp.ID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
log.Printf("[INFO] Successfully updated %v with %v", ip, req)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func resourceCLCPublicIPDelete(d *schema.ResourceData, meta interface{}) error {
|
||||||
|
client := meta.(*clc.Client)
|
||||||
|
s := d.Get("server_id").(string)
|
||||||
|
ip := d.Id()
|
||||||
|
log.Printf("[INFO] Deleting public ip %v", ip)
|
||||||
|
resp, err := client.Server.DeletePublicIP(s, ip)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Failed deleting public ip: %v", err)
|
||||||
|
}
|
||||||
|
err = waitStatus(client, resp.ID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
log.Printf("[INFO] Public IP sucessfully deleted: %v", ip)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseIPSpec(d *schema.ResourceData) (*[]server.Port, *[]server.SourceRestriction) {
|
||||||
|
var ports []server.Port
|
||||||
|
var sources []server.SourceRestriction
|
||||||
|
if v := d.Get("ports"); v != nil {
|
||||||
|
for _, v := range v.([]interface{}) {
|
||||||
|
m := v.(map[string]interface{})
|
||||||
|
p := server.Port{}
|
||||||
|
port, err := strconv.Atoi(m["port"].(string))
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[WARN] Failed parsing port '%v'. skipping", m["port"])
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
p.Protocol = m["protocol"].(string)
|
||||||
|
p.Port = port
|
||||||
|
through := -1
|
||||||
|
if to := m["port_to"]; to != nil {
|
||||||
|
through, _ = strconv.Atoi(to.(string))
|
||||||
|
log.Printf("[DEBUG] port range: %v-%v", port, through)
|
||||||
|
p.PortTo = through
|
||||||
|
}
|
||||||
|
ports = append(ports, p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if v := d.Get("source_restrictions"); v != nil {
|
||||||
|
for _, v := range v.([]interface{}) {
|
||||||
|
m := v.(map[string]interface{})
|
||||||
|
r := server.SourceRestriction{}
|
||||||
|
r.CIDR = m["cidr"].(string)
|
||||||
|
sources = append(sources, r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return &ports, &sources
|
||||||
|
}
|
|
@ -0,0 +1,160 @@
|
||||||
|
package clc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
clc "github.com/CenturyLinkCloud/clc-sdk"
|
||||||
|
"github.com/CenturyLinkCloud/clc-sdk/server"
|
||||||
|
"github.com/hashicorp/terraform/helper/resource"
|
||||||
|
"github.com/hashicorp/terraform/terraform"
|
||||||
|
)
|
||||||
|
|
||||||
|
// things to test:
|
||||||
|
// maps to internal specified ip
|
||||||
|
// port range
|
||||||
|
// update existing rule
|
||||||
|
// CIDR restriction
|
||||||
|
|
||||||
|
func TestAccPublicIPBasic(t *testing.T) {
|
||||||
|
var resp server.PublicIP
|
||||||
|
resource.Test(t, resource.TestCase{
|
||||||
|
PreCheck: func() { testAccPreCheck(t) },
|
||||||
|
Providers: testAccProviders,
|
||||||
|
CheckDestroy: testAccCheckPublicIPDestroy,
|
||||||
|
Steps: []resource.TestStep{
|
||||||
|
resource.TestStep{
|
||||||
|
Config: testAccCheckPublicIPConfigBasic,
|
||||||
|
Check: resource.ComposeTestCheckFunc(
|
||||||
|
testAccCheckPublicIPExists("clc_public_ip.acc_test_public_ip", &resp),
|
||||||
|
testAccCheckPublicIPNIC("clc_public_ip.acc_test_public_ip", &resp),
|
||||||
|
testAccCheckPublicIPPortRange("clc_public_ip.acc_test_public_ip", &resp),
|
||||||
|
testAccCheckPublicIPBlockCIDR("clc_public_ip.acc_test_public_ip", &resp),
|
||||||
|
//testAccCheckPublicIPUpdated("clc_public_ip.eip", &resp),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func testAccCheckPublicIPDestroy(s *terraform.State) error {
|
||||||
|
client := testAccProvider.Meta().(*clc.Client)
|
||||||
|
for _, rs := range s.RootModule().Resources {
|
||||||
|
if rs.Type != "clc_public_ip" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
sid := rs.Primary.Attributes["server_id"]
|
||||||
|
_, err := client.Server.GetPublicIP(sid, rs.Primary.ID)
|
||||||
|
if err == nil {
|
||||||
|
return fmt.Errorf("IP still exists")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func testAccCheckPublicIPExists(n string, resp *server.PublicIP) 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 PublicIP ID is set")
|
||||||
|
}
|
||||||
|
client := testAccProvider.Meta().(*clc.Client)
|
||||||
|
sid := rs.Primary.Attributes["server_id"]
|
||||||
|
p, err := client.Server.GetPublicIP(sid, rs.Primary.ID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
*resp = *p
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testAccCheckPublicIPPortRange(n string, resp *server.PublicIP) resource.TestCheckFunc {
|
||||||
|
return func(s *terraform.State) error {
|
||||||
|
// check the passed port range made it through
|
||||||
|
var spec server.Port
|
||||||
|
for _, p := range resp.Ports {
|
||||||
|
if p.Protocol == "UDP" {
|
||||||
|
spec = p
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if spec.Port != 53 || spec.PortTo != 55 {
|
||||||
|
return fmt.Errorf("Expected udp ports from 53-55 but found: %v", spec)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func testAccCheckPublicIPBlockCIDR(n string, resp *server.PublicIP) resource.TestCheckFunc {
|
||||||
|
return func(s *terraform.State) error {
|
||||||
|
// check the passed port range made it through
|
||||||
|
spec := resp.SourceRestrictions[0]
|
||||||
|
if spec.CIDR != "108.19.67.15/32" {
|
||||||
|
return fmt.Errorf("Expected cidr restriction but found: %v", spec)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testAccCheckPublicIPNIC(n string, resp *server.PublicIP) resource.TestCheckFunc {
|
||||||
|
return func(s *terraform.State) error {
|
||||||
|
rs, ok := s.RootModule().Resources[n]
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("Not found: %s", n)
|
||||||
|
}
|
||||||
|
sid := rs.Primary.Attributes["server_id"]
|
||||||
|
nic := rs.Primary.Attributes["internal_ip_address"]
|
||||||
|
|
||||||
|
client := testAccProvider.Meta().(*clc.Client)
|
||||||
|
srv, err := client.Server.Get(sid)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Failed fetching server? %v", err)
|
||||||
|
}
|
||||||
|
first := srv.Details.IPaddresses[0].Internal
|
||||||
|
if nic != first {
|
||||||
|
return fmt.Errorf("Expected public ip to be mapped to %s but found: %s", first, nic)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var testAccCheckPublicIPConfigBasic = `
|
||||||
|
variable "dc" { default = "IL1" }
|
||||||
|
|
||||||
|
resource "clc_group" "acc_test_group_ip" {
|
||||||
|
location_id = "${var.dc}"
|
||||||
|
name = "acc_test_group_ip"
|
||||||
|
parent = "Default Group"
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "clc_server" "acc_test_server" {
|
||||||
|
name_template = "test"
|
||||||
|
source_server_id = "UBUNTU-14-64-TEMPLATE"
|
||||||
|
group_id = "${clc_group.acc_test_group_ip.id}"
|
||||||
|
cpu = 1
|
||||||
|
memory_mb = 1024
|
||||||
|
password = "Green123$"
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "clc_public_ip" "acc_test_public_ip" {
|
||||||
|
server_id = "${clc_server.acc_test_server.id}"
|
||||||
|
internal_ip_address = "${clc_server.acc_test_server.private_ip_address}"
|
||||||
|
source_restrictions
|
||||||
|
{ cidr = "108.19.67.15/32" }
|
||||||
|
ports
|
||||||
|
{
|
||||||
|
protocol = "TCP"
|
||||||
|
port = 80
|
||||||
|
}
|
||||||
|
ports
|
||||||
|
{
|
||||||
|
protocol = "UDP"
|
||||||
|
port = 53
|
||||||
|
port_to = 55
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
|
@ -0,0 +1,327 @@
|
||||||
|
package clc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
clc "github.com/CenturyLinkCloud/clc-sdk"
|
||||||
|
"github.com/CenturyLinkCloud/clc-sdk/api"
|
||||||
|
"github.com/CenturyLinkCloud/clc-sdk/server"
|
||||||
|
"github.com/CenturyLinkCloud/clc-sdk/status"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform/helper/schema"
|
||||||
|
)
|
||||||
|
|
||||||
|
func resourceCLCServer() *schema.Resource {
|
||||||
|
return &schema.Resource{
|
||||||
|
Create: resourceCLCServerCreate,
|
||||||
|
Read: resourceCLCServerRead,
|
||||||
|
Update: resourceCLCServerUpdate,
|
||||||
|
Delete: resourceCLCServerDelete,
|
||||||
|
Schema: map[string]*schema.Schema{
|
||||||
|
"name_template": &schema.Schema{
|
||||||
|
Type: schema.TypeString,
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
"name": &schema.Schema{
|
||||||
|
Type: schema.TypeString,
|
||||||
|
Computed: true,
|
||||||
|
},
|
||||||
|
"group_id": &schema.Schema{
|
||||||
|
Type: schema.TypeString,
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
"source_server_id": &schema.Schema{
|
||||||
|
Type: schema.TypeString,
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
"cpu": &schema.Schema{
|
||||||
|
Type: schema.TypeInt,
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
"memory_mb": &schema.Schema{
|
||||||
|
Type: schema.TypeInt,
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
"password": &schema.Schema{
|
||||||
|
Type: schema.TypeString,
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
// optional
|
||||||
|
"description": &schema.Schema{
|
||||||
|
Type: schema.TypeString,
|
||||||
|
Optional: true,
|
||||||
|
Default: "",
|
||||||
|
},
|
||||||
|
"type": &schema.Schema{
|
||||||
|
Type: schema.TypeString,
|
||||||
|
Optional: true,
|
||||||
|
Default: "standard",
|
||||||
|
},
|
||||||
|
"network_id": &schema.Schema{
|
||||||
|
Type: schema.TypeString,
|
||||||
|
Optional: true,
|
||||||
|
},
|
||||||
|
"custom_fields": &schema.Schema{
|
||||||
|
Type: schema.TypeList,
|
||||||
|
Optional: true,
|
||||||
|
Elem: &schema.Schema{Type: schema.TypeMap},
|
||||||
|
},
|
||||||
|
"additional_disks": &schema.Schema{
|
||||||
|
Type: schema.TypeList,
|
||||||
|
Optional: true,
|
||||||
|
Elem: &schema.Schema{Type: schema.TypeMap},
|
||||||
|
},
|
||||||
|
|
||||||
|
// optional: misc state storage. non-CLC field
|
||||||
|
"metadata": &schema.Schema{
|
||||||
|
Type: schema.TypeMap,
|
||||||
|
Optional: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
// optional
|
||||||
|
"storage_type": &schema.Schema{
|
||||||
|
Type: schema.TypeString,
|
||||||
|
Optional: true,
|
||||||
|
Default: "standard",
|
||||||
|
},
|
||||||
|
|
||||||
|
// sorta computed
|
||||||
|
"private_ip_address": &schema.Schema{
|
||||||
|
Type: schema.TypeString,
|
||||||
|
Optional: true,
|
||||||
|
Computed: true,
|
||||||
|
Default: nil,
|
||||||
|
},
|
||||||
|
"power_state": &schema.Schema{
|
||||||
|
Type: schema.TypeString,
|
||||||
|
Optional: true,
|
||||||
|
Computed: true,
|
||||||
|
Default: nil,
|
||||||
|
},
|
||||||
|
|
||||||
|
// computed
|
||||||
|
"created_date": &schema.Schema{
|
||||||
|
Type: schema.TypeString,
|
||||||
|
Computed: true,
|
||||||
|
},
|
||||||
|
"modified_date": &schema.Schema{
|
||||||
|
Type: schema.TypeString,
|
||||||
|
Computed: true,
|
||||||
|
},
|
||||||
|
"public_ip_address": &schema.Schema{
|
||||||
|
// RO: if a public_ip is on this server, populate it
|
||||||
|
Type: schema.TypeString,
|
||||||
|
Computed: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func resourceCLCServerCreate(d *schema.ResourceData, meta interface{}) error {
|
||||||
|
client := meta.(*clc.Client)
|
||||||
|
spec := server.Server{
|
||||||
|
Name: d.Get("name_template").(string),
|
||||||
|
Password: d.Get("password").(string),
|
||||||
|
Description: d.Get("description").(string),
|
||||||
|
GroupID: d.Get("group_id").(string),
|
||||||
|
CPU: d.Get("cpu").(int),
|
||||||
|
MemoryGB: d.Get("memory_mb").(int) / 1024,
|
||||||
|
SourceServerID: d.Get("source_server_id").(string),
|
||||||
|
Type: d.Get("type").(string),
|
||||||
|
IPaddress: d.Get("private_ip_address").(string),
|
||||||
|
NetworkID: d.Get("network_id").(string),
|
||||||
|
Storagetype: d.Get("storage_type").(string),
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
disks, err := parseAdditionalDisks(d)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Failed parsing disks: %v", err)
|
||||||
|
}
|
||||||
|
spec.Additionaldisks = disks
|
||||||
|
fields, err := parseCustomFields(d)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Failed setting customfields: %v", err)
|
||||||
|
}
|
||||||
|
spec.Customfields = fields
|
||||||
|
|
||||||
|
resp, err := client.Server.Create(spec)
|
||||||
|
if err != nil || !resp.IsQueued {
|
||||||
|
return fmt.Errorf("Failed creating server: %v", err)
|
||||||
|
}
|
||||||
|
// server's UUID returned under rel=self link
|
||||||
|
_, uuid := resp.Links.GetID("self")
|
||||||
|
|
||||||
|
ok, st := resp.GetStatusID()
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("Failed extracting status to poll on %v: %v", resp, err)
|
||||||
|
}
|
||||||
|
err = waitStatus(client, st)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
s, err := client.Server.Get(uuid)
|
||||||
|
d.SetId(strings.ToUpper(s.Name))
|
||||||
|
log.Printf("[INFO] Server created. id: %v", s.Name)
|
||||||
|
return resourceCLCServerRead(d, meta)
|
||||||
|
}
|
||||||
|
|
||||||
|
func resourceCLCServerRead(d *schema.ResourceData, meta interface{}) error {
|
||||||
|
client := meta.(*clc.Client)
|
||||||
|
s, err := client.Server.Get(d.Id())
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[INFO] Failed finding server: %v. Marking destroyed", d.Id())
|
||||||
|
d.SetId("")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if len(s.Details.IPaddresses) > 0 {
|
||||||
|
d.Set("private_ip_address", s.Details.IPaddresses[0].Internal)
|
||||||
|
if "" != s.Details.IPaddresses[0].Public {
|
||||||
|
d.Set("public_ip_address", s.Details.IPaddresses[0].Public)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
d.Set("name", s.Name)
|
||||||
|
d.Set("groupId", s.GroupID)
|
||||||
|
d.Set("status", s.Status)
|
||||||
|
d.Set("power_state", s.Details.Powerstate)
|
||||||
|
d.Set("cpu", s.Details.CPU)
|
||||||
|
d.Set("memory_mb", s.Details.MemoryMB)
|
||||||
|
d.Set("disk_gb", s.Details.Storagegb)
|
||||||
|
d.Set("status", s.Status)
|
||||||
|
d.Set("storage_type", s.Storagetype)
|
||||||
|
d.Set("created_date", s.ChangeInfo.CreatedDate)
|
||||||
|
d.Set("modified_date", s.ChangeInfo.ModifiedDate)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func resourceCLCServerUpdate(d *schema.ResourceData, meta interface{}) error {
|
||||||
|
client := meta.(*clc.Client)
|
||||||
|
id := d.Id()
|
||||||
|
|
||||||
|
var err error
|
||||||
|
var edits []api.Update
|
||||||
|
var updates []api.Update
|
||||||
|
var i int
|
||||||
|
|
||||||
|
poll := make(chan *status.Response, 1)
|
||||||
|
d.Partial(true)
|
||||||
|
s, err := client.Server.Get(id)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Failed fetching server: %v - %v", d.Id(), err)
|
||||||
|
}
|
||||||
|
// edits happen synchronously
|
||||||
|
if delta, orig := d.Get("description").(string), s.Description; delta != orig {
|
||||||
|
d.SetPartial("description")
|
||||||
|
edits = append(edits, server.UpdateDescription(delta))
|
||||||
|
}
|
||||||
|
if delta, orig := d.Get("group_id").(string), s.GroupID; delta != orig {
|
||||||
|
d.SetPartial("group_id")
|
||||||
|
edits = append(edits, server.UpdateGroup(delta))
|
||||||
|
}
|
||||||
|
if len(edits) > 0 {
|
||||||
|
err = client.Server.Edit(id, edits...)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Failed saving edits: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// updates are queue processed
|
||||||
|
if d.HasChange("password") {
|
||||||
|
d.SetPartial("password")
|
||||||
|
o, _ := d.GetChange("password")
|
||||||
|
old := o.(string)
|
||||||
|
pass := d.Get("password").(string)
|
||||||
|
updates = append(updates, server.UpdateCredentials(old, pass))
|
||||||
|
}
|
||||||
|
if i = d.Get("cpu").(int); i != s.Details.CPU {
|
||||||
|
d.SetPartial("cpu")
|
||||||
|
updates = append(updates, server.UpdateCPU(i))
|
||||||
|
}
|
||||||
|
if i = d.Get("memory_mb").(int); i != s.Details.MemoryMB {
|
||||||
|
d.SetPartial("memory_mb")
|
||||||
|
updates = append(updates, server.UpdateMemory(i/1024)) // takes GB
|
||||||
|
}
|
||||||
|
|
||||||
|
if d.HasChange("custom_fields") {
|
||||||
|
d.SetPartial("custom_fields")
|
||||||
|
fields, err := parseCustomFields(d)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Failed setting customfields: %v", err)
|
||||||
|
}
|
||||||
|
updates = append(updates, server.UpdateCustomfields(fields))
|
||||||
|
}
|
||||||
|
if d.HasChange("additional_disks") {
|
||||||
|
d.SetPartial("additional_disks")
|
||||||
|
disks, err := parseAdditionalDisks(d)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Failed parsing disks: %v", err)
|
||||||
|
}
|
||||||
|
updates = append(updates, server.UpdateAdditionaldisks(disks))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(updates) > 0 {
|
||||||
|
resp, err := client.Server.Update(id, updates...)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Failed saving updates: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = client.Status.Poll(resp.ID, poll)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
status := <-poll
|
||||||
|
if status.Failed() {
|
||||||
|
return fmt.Errorf("Update failed")
|
||||||
|
}
|
||||||
|
log.Printf("[INFO] Server updated! status: %v", status.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
if d.HasChange("power_state") {
|
||||||
|
st := d.Get("power_state").(string)
|
||||||
|
log.Printf("[DEBUG] POWER: %v => %v", s.Details.Powerstate, st)
|
||||||
|
newst := stateFromString(st)
|
||||||
|
servers, err := client.Server.PowerState(newst, s.Name)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Failed setting power state to: %v", newst)
|
||||||
|
}
|
||||||
|
ok, id := servers[0].GetStatusID()
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("Failed extracting power state queue status from: %v", servers[0])
|
||||||
|
}
|
||||||
|
err = client.Status.Poll(id, poll)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
status := <-poll
|
||||||
|
if status.Failed() {
|
||||||
|
return fmt.Errorf("Update failed")
|
||||||
|
}
|
||||||
|
log.Printf("[INFO] state updated: %v", status)
|
||||||
|
}
|
||||||
|
|
||||||
|
d.Partial(false)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func resourceCLCServerDelete(d *schema.ResourceData, meta interface{}) error {
|
||||||
|
client := meta.(*clc.Client)
|
||||||
|
id := d.Id()
|
||||||
|
resp, err := client.Server.Delete(id)
|
||||||
|
if err != nil || !resp.IsQueued {
|
||||||
|
return fmt.Errorf("Failed queueing delete of %v - %v", id, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ok, st := resp.GetStatusID()
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("Failed extracting status to poll on %v: %v", resp, err)
|
||||||
|
}
|
||||||
|
err = waitStatus(client, st)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
log.Printf("[INFO] Server sucessfully deleted: %v", st)
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,264 @@
|
||||||
|
package clc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
clc "github.com/CenturyLinkCloud/clc-sdk"
|
||||||
|
"github.com/CenturyLinkCloud/clc-sdk/server"
|
||||||
|
"github.com/hashicorp/terraform/helper/resource"
|
||||||
|
"github.com/hashicorp/terraform/terraform"
|
||||||
|
)
|
||||||
|
|
||||||
|
// things to test:
|
||||||
|
// basic crud
|
||||||
|
// modify specs
|
||||||
|
// power operations
|
||||||
|
// add'l disks
|
||||||
|
// custom fields? (skip)
|
||||||
|
|
||||||
|
func TestAccServerBasic(t *testing.T) {
|
||||||
|
var resp server.Response
|
||||||
|
resource.Test(t, resource.TestCase{
|
||||||
|
PreCheck: func() { testAccPreCheck(t) },
|
||||||
|
Providers: testAccProviders,
|
||||||
|
CheckDestroy: testAccCheckServerDestroy,
|
||||||
|
Steps: []resource.TestStep{
|
||||||
|
resource.TestStep{
|
||||||
|
Config: testAccCheckServerConfigBasic,
|
||||||
|
Check: resource.ComposeTestCheckFunc(
|
||||||
|
testAccCheckServerExists("clc_server.acc_test_server", &resp),
|
||||||
|
resource.TestCheckResourceAttr(
|
||||||
|
"clc_server.acc_test_server", "name_template", "test"),
|
||||||
|
resource.TestCheckResourceAttr(
|
||||||
|
"clc_server.acc_test_server", "cpu", "1"),
|
||||||
|
resource.TestCheckResourceAttr(
|
||||||
|
"clc_server.acc_test_server", "memory_mb", "1024"),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
// update simple attrs
|
||||||
|
resource.TestStep{
|
||||||
|
Config: testAccCheckServerConfigCPUMEM,
|
||||||
|
Check: resource.ComposeTestCheckFunc(
|
||||||
|
testAccCheckServerExists("clc_server.acc_test_server", &resp),
|
||||||
|
resource.TestCheckResourceAttr(
|
||||||
|
"clc_server.acc_test_server", "cpu", "2"),
|
||||||
|
resource.TestCheckResourceAttr(
|
||||||
|
"clc_server.acc_test_server", "memory_mb", "2048"),
|
||||||
|
testAccCheckServerUpdatedSpec("clc_server.acc_test_server", &resp),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
// toggle power
|
||||||
|
resource.TestStep{
|
||||||
|
Config: testAccCheckServerConfigPower,
|
||||||
|
Check: resource.ComposeTestCheckFunc(
|
||||||
|
testAccCheckServerExists("clc_server.acc_test_server", &resp),
|
||||||
|
resource.TestCheckResourceAttr(
|
||||||
|
"clc_server.acc_test_server", "power_state", "stopped"),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
/* // currently broken since disk updates require diskId
|
||||||
|
// add disks
|
||||||
|
resource.TestStep{
|
||||||
|
Config: testAccCheckServerConfig_disks,
|
||||||
|
Check: resource.ComposeTestCheckFunc(
|
||||||
|
testAccCheckServerExists("clc_server.acc_test_server", &resp),
|
||||||
|
// power still off
|
||||||
|
resource.TestCheckResourceAttr(
|
||||||
|
"clc_server.acc_test_server", "power_state", "stopped"),
|
||||||
|
testAccCheckServerUpdatedDisks("clc_server.acc_test_server", &resp),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
*/
|
||||||
|
/* // broken since network id is a (account-specific) guid
|
||||||
|
// set network id
|
||||||
|
resource.TestStep{
|
||||||
|
Config: testAccCheckServerConfigNetwork,
|
||||||
|
Check: resource.ComposeTestCheckFunc(
|
||||||
|
testAccCheckServerExists("clc_server.acc_test_server", &resp),
|
||||||
|
resource.TestCheckResourceAttr(
|
||||||
|
"clc_server.acc_test_server", "network_id", "15a0f669c332435ebf375e010ac79fbb"),
|
||||||
|
testAccCheckServerUpdatedSpec("clc_server.acc_test_server", &resp),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
*/
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func testAccCheckServerDestroy(s *terraform.State) error {
|
||||||
|
client := testAccProvider.Meta().(*clc.Client)
|
||||||
|
|
||||||
|
for _, rs := range s.RootModule().Resources {
|
||||||
|
if rs.Type != "clc_server" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := client.Server.Get(rs.Primary.ID)
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
return fmt.Errorf("Server still exists")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func testAccCheckServerExists(n string, resp *server.Response) 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 server ID is set")
|
||||||
|
}
|
||||||
|
|
||||||
|
client := testAccProvider.Meta().(*clc.Client)
|
||||||
|
srv, err := client.Server.Get(rs.Primary.ID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.ToUpper(srv.ID) != rs.Primary.ID {
|
||||||
|
return fmt.Errorf("Server not found")
|
||||||
|
}
|
||||||
|
*resp = *srv
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testAccCheckServerUpdatedSpec(n string, resp *server.Response) resource.TestCheckFunc {
|
||||||
|
return func(s *terraform.State) error {
|
||||||
|
rs, ok := s.RootModule().Resources[n]
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("Not found: %s", n)
|
||||||
|
}
|
||||||
|
client := testAccProvider.Meta().(*clc.Client)
|
||||||
|
srv, err := client.Server.Get(rs.Primary.ID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
cpu := srv.Details.CPU
|
||||||
|
mem := srv.Details.MemoryMB
|
||||||
|
scpu := fmt.Sprintf("%v", cpu)
|
||||||
|
smem := fmt.Sprintf("%v", mem)
|
||||||
|
excpu := rs.Primary.Attributes["cpu"]
|
||||||
|
exmem := rs.Primary.Attributes["memory_mb"]
|
||||||
|
if scpu != excpu {
|
||||||
|
return fmt.Errorf("Expected CPU to be %v but found %v", excpu, scpu)
|
||||||
|
}
|
||||||
|
if smem != exmem {
|
||||||
|
return fmt.Errorf("Expected MEM to be %v but found %v", exmem, smem)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testAccCheckServerUpdatedDisks(n string, resp *server.Response) resource.TestCheckFunc {
|
||||||
|
return func(s *terraform.State) error {
|
||||||
|
rs, ok := s.RootModule().Resources[n]
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("Not found: %s", n)
|
||||||
|
}
|
||||||
|
client := testAccProvider.Meta().(*clc.Client)
|
||||||
|
srv, err := client.Server.Get(rs.Primary.ID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(srv.Details.Disks) <= 3 {
|
||||||
|
return fmt.Errorf("Expected total of > 3 drives. found: %v", len(srv.Details.Disks))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const testAccCheckServerConfigBasic = `
|
||||||
|
variable "dc" { default = "IL1" }
|
||||||
|
|
||||||
|
resource "clc_group" "acc_test_group_server" {
|
||||||
|
location_id = "${var.dc}"
|
||||||
|
name = "acc_test_group_server"
|
||||||
|
parent = "Default Group"
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "clc_server" "acc_test_server" {
|
||||||
|
name_template = "test"
|
||||||
|
source_server_id = "UBUNTU-14-64-TEMPLATE"
|
||||||
|
group_id = "${clc_group.acc_test_group_server.id}"
|
||||||
|
cpu = 1
|
||||||
|
memory_mb = 1024
|
||||||
|
password = "Green123$"
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const testAccCheckServerConfigCPUMEM = `
|
||||||
|
variable "dc" { default = "IL1" }
|
||||||
|
|
||||||
|
resource "clc_group" "acc_test_group_server" {
|
||||||
|
location_id = "${var.dc}"
|
||||||
|
name = "acc_test_group_server"
|
||||||
|
parent = "Default Group"
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "clc_server" "acc_test_server" {
|
||||||
|
name_template = "test"
|
||||||
|
source_server_id = "UBUNTU-14-64-TEMPLATE"
|
||||||
|
group_id = "${clc_group.acc_test_group_server.id}"
|
||||||
|
cpu = 2
|
||||||
|
memory_mb = 2048
|
||||||
|
password = "Green123$"
|
||||||
|
power_state = "started"
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const testAccCheckServerConfigPower = `
|
||||||
|
variable "dc" { default = "IL1" }
|
||||||
|
|
||||||
|
resource "clc_group" "acc_test_group_server" {
|
||||||
|
location_id = "${var.dc}"
|
||||||
|
name = "acc_test_group_server"
|
||||||
|
parent = "Default Group"
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "clc_server" "acc_test_server" {
|
||||||
|
name_template = "test"
|
||||||
|
source_server_id = "UBUNTU-14-64-TEMPLATE"
|
||||||
|
group_id = "${clc_group.acc_test_group_server.id}"
|
||||||
|
cpu = 2
|
||||||
|
memory_mb = 2048
|
||||||
|
password = "Green123$"
|
||||||
|
power_state = "stopped"
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const testAccCheckServerConfigDisks = `
|
||||||
|
variable "dc" { default = "IL1" }
|
||||||
|
|
||||||
|
resource "clc_group" "acc_test_group_server" {
|
||||||
|
location_id = "${var.dc}"
|
||||||
|
name = "acc_test_group_server"
|
||||||
|
parent = "Default Group"
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "clc_server" "acc_test_server" {
|
||||||
|
name_template = "test"
|
||||||
|
source_server_id = "UBUNTU-14-64-TEMPLATE"
|
||||||
|
group_id = "${clc_group.acc_test_group_server.id}"
|
||||||
|
cpu = 2
|
||||||
|
memory_mb = 2048
|
||||||
|
password = "Green123$"
|
||||||
|
power_state = "stopped"
|
||||||
|
# network_id = "15a0f669c332435ebf375e010ac79fbb"
|
||||||
|
additional_disks
|
||||||
|
{
|
||||||
|
path = "/data1"
|
||||||
|
size_gb = 100
|
||||||
|
type = "partitioned"
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
`
|
|
@ -59,6 +59,10 @@
|
||||||
"ImportPath": "github.com/DreamItGetIT/statuscake",
|
"ImportPath": "github.com/DreamItGetIT/statuscake",
|
||||||
"Rev": "8cbe86575f00210a6df2c19cb2f59b00cd181de3"
|
"Rev": "8cbe86575f00210a6df2c19cb2f59b00cd181de3"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"ImportPath": "github.com/CenturyLinkCloud/clc-sdk",
|
||||||
|
"Rev": "74abd779894192c559ad29f0183a1716370490ad"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"ImportPath": "github.com/apparentlymart/go-cidr/cidr",
|
"ImportPath": "github.com/apparentlymart/go-cidr/cidr",
|
||||||
"Rev": "a3ebdb999b831ecb6ab8a226e31b07b2b9061c47"
|
"Rev": "a3ebdb999b831ecb6ab8a226e31b07b2b9061c47"
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
## Creating a standard web application
|
||||||
|
|
||||||
|
This example provides sample configuration for creating the infra for a basic webapp.
|
||||||
|
|
||||||
|
For CLC provider, set up your CLC environment as outlined in https://www.terraform.io/docs/providers/clc/index.html
|
||||||
|
|
||||||
|
Once ready run 'terraform plan' to review.
|
||||||
|
|
||||||
|
Once satisfied with plan, run 'terraform apply'
|
|
@ -0,0 +1,96 @@
|
||||||
|
# --------------------
|
||||||
|
# Credentials
|
||||||
|
provider "clc" {
|
||||||
|
username = "${var.clc_username}"
|
||||||
|
password = "${var.clc_password}"
|
||||||
|
account = "${var.clc_account}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# --------------------
|
||||||
|
# Provision/Resolve a server group
|
||||||
|
resource "clc_group" "frontends" {
|
||||||
|
location_id = "CA1"
|
||||||
|
name = "frontends"
|
||||||
|
parent = "Default Group"
|
||||||
|
}
|
||||||
|
|
||||||
|
# --------------------
|
||||||
|
# Provision a server
|
||||||
|
resource "clc_server" "node" {
|
||||||
|
name_template = "trusty"
|
||||||
|
source_server_id = "UBUNTU-14-64-TEMPLATE"
|
||||||
|
group_id = "${clc_group.frontends.id}"
|
||||||
|
cpu = 2
|
||||||
|
memory_mb = 2048
|
||||||
|
password = "Green123$"
|
||||||
|
additional_disks
|
||||||
|
{
|
||||||
|
path = "/var"
|
||||||
|
size_gb = 100
|
||||||
|
type = "partitioned"
|
||||||
|
}
|
||||||
|
additional_disks
|
||||||
|
{
|
||||||
|
size_gb = 10
|
||||||
|
type = "raw"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# --------------------
|
||||||
|
# Provision a public ip
|
||||||
|
resource "clc_public_ip" "backdoor" {
|
||||||
|
server_id = "${clc_server.node.0.id}"
|
||||||
|
internal_ip_address = "${clc_server.node.0.private_ip_address}"
|
||||||
|
ports
|
||||||
|
{
|
||||||
|
protocol = "ICMP"
|
||||||
|
port = -1
|
||||||
|
}
|
||||||
|
ports
|
||||||
|
{
|
||||||
|
protocol = "TCP"
|
||||||
|
port = 22
|
||||||
|
}
|
||||||
|
source_restrictions
|
||||||
|
{ cidr = "173.60.0.0/16" }
|
||||||
|
|
||||||
|
|
||||||
|
# ssh in and start a simple http server on :8080
|
||||||
|
provisioner "remote-exec" {
|
||||||
|
inline = [
|
||||||
|
"cd /tmp; python -mSimpleHTTPServer > /dev/null 2>&1 &"
|
||||||
|
]
|
||||||
|
connection {
|
||||||
|
host = "${clc_public_ip.backdoor.id}"
|
||||||
|
user = "root"
|
||||||
|
password = "${clc_server.node.password}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------
|
||||||
|
# Provision a load balancer
|
||||||
|
resource "clc_load_balancer" "frontdoor" {
|
||||||
|
data_center = "${clc_group.frontends.location_id}"
|
||||||
|
name = "frontdoor"
|
||||||
|
description = "frontdoor"
|
||||||
|
status = "enabled"
|
||||||
|
}
|
||||||
|
|
||||||
|
# --------------------
|
||||||
|
# Provision a load balancer pool
|
||||||
|
resource "clc_load_balancer_pool" "pool" {
|
||||||
|
data_center = "${clc_group.frontends.location_id}"
|
||||||
|
load_balancer = "${clc_load_balancer.frontdoor.id}"
|
||||||
|
method = "roundRobin"
|
||||||
|
persistence = "standard"
|
||||||
|
port = 80
|
||||||
|
nodes
|
||||||
|
{
|
||||||
|
status = "enabled"
|
||||||
|
ipAddress = "${clc_server.node.private_ip_address}"
|
||||||
|
privatePort = 8000
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,27 @@
|
||||||
|
output "group_id" {
|
||||||
|
value = "${clc_group.frontends.id}"
|
||||||
|
}
|
||||||
|
|
||||||
|
output "node_id" {
|
||||||
|
value = "${clc_server.node.id}"
|
||||||
|
}
|
||||||
|
|
||||||
|
output "node_ip" {
|
||||||
|
value = "${clc_server.node.private_ip_address}"
|
||||||
|
}
|
||||||
|
|
||||||
|
output "node_password" {
|
||||||
|
value = "${clc_server.node.password}"
|
||||||
|
}
|
||||||
|
|
||||||
|
output "backdoor" {
|
||||||
|
value = "${clc_public_ip.backdoor.id}"
|
||||||
|
}
|
||||||
|
|
||||||
|
output "frontdoor" {
|
||||||
|
value = "${clc_load_balancer.frontdoor.ip_address}"
|
||||||
|
}
|
||||||
|
|
||||||
|
output "pool" {
|
||||||
|
value = "curl -vv ${clc_load_balancer.frontdoor.ip_address}"
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
variable "clc_username" {
|
||||||
|
default = "<username>"
|
||||||
|
}
|
||||||
|
variable "clc_password" {
|
||||||
|
default = "<password>"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "clc_account" {
|
||||||
|
default = "<alias>"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Ubuntu 14.04
|
||||||
|
variable "image" {
|
||||||
|
default = "ubuntu-14-64-template"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "app_port" {
|
||||||
|
default = 8080
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
service_name: travis-ci
|
|
@ -0,0 +1,3 @@
|
||||||
|
*.swp
|
||||||
|
*.cov
|
||||||
|
Godeps/_workspace
|
|
@ -0,0 +1,8 @@
|
||||||
|
language: go
|
||||||
|
|
||||||
|
go:
|
||||||
|
- 1.5
|
||||||
|
|
||||||
|
install: make deps
|
||||||
|
script:
|
||||||
|
- make test
|
|
@ -0,0 +1,190 @@
|
||||||
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
control with that entity. For the purposes of this definition,
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
Copyright 2015 Mike Beyer
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
|
@ -0,0 +1,12 @@
|
||||||
|
VERSION=0.1
|
||||||
|
|
||||||
|
.PHONY : test cover deps
|
||||||
|
test:
|
||||||
|
godep go test ./...
|
||||||
|
cover:
|
||||||
|
./cover.sh
|
||||||
|
deps:
|
||||||
|
go get github.com/tools/godep
|
||||||
|
go get golang.org/x/tools/cmd/goimports
|
||||||
|
go get github.com/mattn/goveralls
|
||||||
|
godep restore
|
|
@ -0,0 +1,89 @@
|
||||||
|
CLC SDK (for go!) [![Build Status](https://travis-ci.org/CenturyLinkCloud/clc-sdk.svg?branch=master)](https://travis-ci.org/CenturyLinkCloud/clc-sdk) [![Coverage Status](https://coveralls.io/repos/mikebeyer/clc-sdk/badge.svg?branch=master&service=github)](https://coveralls.io/github/mikebeyer/clc-sdk?branch=master)
|
||||||
|
======
|
||||||
|
|
||||||
|
Installation
|
||||||
|
---------------------
|
||||||
|
|
||||||
|
```sh
|
||||||
|
$ go get github.com/CenturyLinkCloud/clc-sdk
|
||||||
|
$ make deps
|
||||||
|
$ make test
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
Configuration
|
||||||
|
-------
|
||||||
|
The SDK supports the following helpers for creating your configuration
|
||||||
|
|
||||||
|
|
||||||
|
Reading from the environment
|
||||||
|
|
||||||
|
```go
|
||||||
|
config, _ := api.EnvConfig()
|
||||||
|
```
|
||||||
|
|
||||||
|
Reading from a file
|
||||||
|
|
||||||
|
|
||||||
|
```go
|
||||||
|
config, _ := api.FileConfig("./config.json")
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
Direct configuration
|
||||||
|
|
||||||
|
```go
|
||||||
|
config, _ := api.NewConfig(un, pwd)
|
||||||
|
// defaults:
|
||||||
|
config.Alias = "" // resolved on Authentication
|
||||||
|
config.UserAgent = "CenturyLinkCloud/clc-sdk"
|
||||||
|
config.BaseURI = "https://api.ctl.io/v2"
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
Enable http wire tracing with env var `DEBUG=on`.
|
||||||
|
|
||||||
|
Additionally, callers of the SDK should set `config.UserAgent` to identify to platform appropriately.
|
||||||
|
|
||||||
|
|
||||||
|
Examples
|
||||||
|
-------
|
||||||
|
To create a new server
|
||||||
|
|
||||||
|
```go
|
||||||
|
client := clc.New(api.EnvConfig())
|
||||||
|
|
||||||
|
server := server.Server{
|
||||||
|
Name: "server",
|
||||||
|
CPU: 1,
|
||||||
|
MemoryGB: 1,
|
||||||
|
GroupID: "GROUP-ID",
|
||||||
|
SourceServerID: "UBUNTU-14-64-TEMPLATE",
|
||||||
|
Type: "standard",
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, _ := client.Server.Create(server)
|
||||||
|
```
|
||||||
|
|
||||||
|
Check status of a server build
|
||||||
|
|
||||||
|
```go
|
||||||
|
resp, _ := client.Server.Create(server)
|
||||||
|
|
||||||
|
status, _ := client.Status.Get(resp.GetStatusID())
|
||||||
|
```
|
||||||
|
|
||||||
|
Async polling for complection
|
||||||
|
|
||||||
|
```go
|
||||||
|
resp, _ := client.Server.Create(server)
|
||||||
|
|
||||||
|
poll := make(chan *status.Response, 1)
|
||||||
|
service.Status.Poll(resp.GetStatusID(), poll)
|
||||||
|
|
||||||
|
status := <- poll
|
||||||
|
```
|
||||||
|
|
||||||
|
License
|
||||||
|
-------
|
||||||
|
This project is licensed under the [Apache License v2.0](http://www.apache.org/licenses/LICENSE-2.0.html).
|
|
@ -0,0 +1,67 @@
|
||||||
|
package aa
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/CenturyLinkCloud/clc-sdk/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
func New(client api.HTTP) *Service {
|
||||||
|
return &Service{
|
||||||
|
client: client,
|
||||||
|
config: client.Config(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Service struct {
|
||||||
|
client api.HTTP
|
||||||
|
config *api.Config
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) Get(id string) (*Policy, error) {
|
||||||
|
url := fmt.Sprintf("%s/antiAffinityPolicies/%s/%s", s.config.BaseURL, s.config.Alias, id)
|
||||||
|
policy := &Policy{}
|
||||||
|
err := s.client.Get(url, policy)
|
||||||
|
return policy, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) GetAll() (*Policies, error) {
|
||||||
|
url := fmt.Sprintf("%s/antiAffinityPolicies/%s", s.config.BaseURL, s.config.Alias)
|
||||||
|
policies := &Policies{}
|
||||||
|
err := s.client.Get(url, policies)
|
||||||
|
return policies, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) Create(name, location string) (*Policy, error) {
|
||||||
|
policy := &Policy{Name: name, Location: location}
|
||||||
|
resp := &Policy{}
|
||||||
|
url := fmt.Sprintf("%s/antiAffinityPolicies/%s", s.config.BaseURL, s.config.Alias)
|
||||||
|
err := s.client.Post(url, policy, resp)
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) Update(id string, name string) (*Policy, error) {
|
||||||
|
policy := &Policy{Name: name}
|
||||||
|
resp := &Policy{}
|
||||||
|
url := fmt.Sprintf("%s/antiAffinityPolicies/%s/%s", s.config.BaseURL, s.config.Alias, id)
|
||||||
|
err := s.client.Put(url, policy, resp)
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) Delete(id string) error {
|
||||||
|
url := fmt.Sprintf("%s/antiAffinityPolicies/%s/%s", s.config.BaseURL, s.config.Alias, id)
|
||||||
|
err := s.client.Delete(url, nil)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
type Policy struct {
|
||||||
|
ID string `json:"id,omitempty"`
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
Location string `json:"location,omitempty"`
|
||||||
|
Links api.Links `json:"links,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Policies struct {
|
||||||
|
Items []Policy `json:"items,omitempty"`
|
||||||
|
Links api.Links `json:"links,omitempty"`
|
||||||
|
}
|
|
@ -0,0 +1,80 @@
|
||||||
|
package alert
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/CenturyLinkCloud/clc-sdk/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
func New(client api.HTTP) *Service {
|
||||||
|
return &Service{
|
||||||
|
client: client,
|
||||||
|
config: client.Config(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Service struct {
|
||||||
|
client api.HTTP
|
||||||
|
config *api.Config
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) Get(id string) (*Alert, error) {
|
||||||
|
url := fmt.Sprintf("%s/alertPolicies/%s/%s", s.config.BaseURL, s.config.Alias, id)
|
||||||
|
resp := &Alert{}
|
||||||
|
err := s.client.Get(url, resp)
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) GetAll() (*Alerts, error) {
|
||||||
|
url := fmt.Sprintf("%s/alertPolicies/%s", s.config.BaseURL, s.config.Alias)
|
||||||
|
resp := &Alerts{}
|
||||||
|
err := s.client.Get(url, resp)
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) Create(alert Alert) (*Alert, error) {
|
||||||
|
url := fmt.Sprintf("%s/alertPolicies/%s", s.config.BaseURL, s.config.Alias)
|
||||||
|
resp := &Alert{}
|
||||||
|
err := s.client.Post(url, alert, resp)
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) Update(id string, alert Alert) (*Alert, error) {
|
||||||
|
url := fmt.Sprintf("%s/alertPolicies/%s/%s", s.config.BaseURL, s.config.Alias, id)
|
||||||
|
resp := &Alert{}
|
||||||
|
err := s.client.Put(url, alert, resp)
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) Delete(id string) error {
|
||||||
|
url := fmt.Sprintf("%s/alertPolicies/%s/%s", s.config.BaseURL, s.config.Alias, id)
|
||||||
|
return s.client.Delete(url, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Alerts struct {
|
||||||
|
Items []Alert `json:"items"`
|
||||||
|
Links api.Links `json:"links"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Alert struct {
|
||||||
|
ID string `json:"id,omitempty"`
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
Actions []Action `json:"actions,omitempty"`
|
||||||
|
Triggers []Trigger `json:"triggers,omitempty"`
|
||||||
|
Links api.Links `json:"links,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Action struct {
|
||||||
|
Action string `json:"action"`
|
||||||
|
Setting Setting `json:"settings"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Setting struct {
|
||||||
|
Recipients []string `json:"recipients"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Trigger struct {
|
||||||
|
Metric string `json:"metric"`
|
||||||
|
Duration string `json:"duration"`
|
||||||
|
Threshold float64 `json:"threshold"`
|
||||||
|
}
|
|
@ -0,0 +1,239 @@
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httputil"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
var debug = os.Getenv("DEBUG") != ""
|
||||||
|
|
||||||
|
const baseUriDefault = "https://api.ctl.io/v2"
|
||||||
|
const userAgentDefault = "CenturyLinkCloud/clc-sdk"
|
||||||
|
|
||||||
|
func New(config Config) *Client {
|
||||||
|
return &Client{
|
||||||
|
config: config,
|
||||||
|
client: http.DefaultClient,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type HTTP interface {
|
||||||
|
Get(url string, resp interface{}) error
|
||||||
|
Post(url string, body, resp interface{}) error
|
||||||
|
Put(url string, body, resp interface{}) error
|
||||||
|
Patch(url string, body, resp interface{}) error
|
||||||
|
Delete(url string, resp interface{}) error
|
||||||
|
Config() *Config
|
||||||
|
}
|
||||||
|
|
||||||
|
type Client struct {
|
||||||
|
config Config
|
||||||
|
Token Token
|
||||||
|
|
||||||
|
client *http.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) Config() *Config {
|
||||||
|
return &c.config
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) Get(url string, resp interface{}) error {
|
||||||
|
return c.DoWithAuth("GET", url, nil, resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) Post(url string, body, resp interface{}) error {
|
||||||
|
return c.DoWithAuth("POST", url, body, resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) Put(url string, body, resp interface{}) error {
|
||||||
|
return c.DoWithAuth("PUT", url, body, resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) Patch(url string, body, resp interface{}) error {
|
||||||
|
return c.DoWithAuth("PATCH", url, body, resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) Delete(url string, resp interface{}) error {
|
||||||
|
return c.DoWithAuth("DELETE", url, nil, resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) Auth() error {
|
||||||
|
url := fmt.Sprintf("%s/authentication/login", c.config.BaseURL)
|
||||||
|
body, err := c.serialize(c.config.User)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequest("POST", url, body)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = c.Do(req, &c.Token)
|
||||||
|
if err == nil {
|
||||||
|
// set Alias from returned token
|
||||||
|
c.config.Alias = c.Token.Alias
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) Do(req *http.Request, ret interface{}) error {
|
||||||
|
if debug {
|
||||||
|
v, _ := httputil.DumpRequest(req, true)
|
||||||
|
log.Println(string(v))
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Add("User-Agent", c.config.UserAgent)
|
||||||
|
req.Header.Add("Accept", "application/json")
|
||||||
|
if req.Body != nil {
|
||||||
|
req.Header.Add("Content-Type", "application/json")
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := c.client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if debug && resp != nil {
|
||||||
|
v, _ := httputil.DumpResponse(resp, true)
|
||||||
|
log.Println(string(v))
|
||||||
|
}
|
||||||
|
if resp.StatusCode >= 400 {
|
||||||
|
b, _ := ioutil.ReadAll(resp.Body)
|
||||||
|
return fmt.Errorf("http err: [%s] - %s", resp.Status, b)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ret == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME? empty body: check status=204 or content-length=0 before parsing
|
||||||
|
return json.NewDecoder(resp.Body).Decode(ret)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) DoWithAuth(method, url string, body, ret interface{}) error {
|
||||||
|
if !c.Token.Valid() {
|
||||||
|
err := c.Auth()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
b, err := c.serialize(body)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequest(method, url, b)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
req.Header.Add("Authorization", "Bearer "+c.Token.Token)
|
||||||
|
|
||||||
|
return c.Do(req, ret)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) serialize(body interface{}) (io.Reader, error) {
|
||||||
|
if body == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
b := new(bytes.Buffer)
|
||||||
|
err := json.NewEncoder(b).Encode(body)
|
||||||
|
return b, err
|
||||||
|
}
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
User User `json:"user"`
|
||||||
|
Alias string `json:"alias"`
|
||||||
|
BaseURL *url.URL `json:"-"`
|
||||||
|
UserAgent string `json:"agent,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Config) Valid() bool {
|
||||||
|
return c.User.Username != "" && c.User.Password != "" && c.BaseURL != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func EnvConfig() (Config, error) {
|
||||||
|
user := os.Getenv("CLC_USERNAME")
|
||||||
|
pass := os.Getenv("CLC_PASSWORD")
|
||||||
|
config, err := NewConfig(user, pass)
|
||||||
|
if err != nil {
|
||||||
|
return config, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !config.Valid() {
|
||||||
|
return config, fmt.Errorf("missing environment variables [%s]", config)
|
||||||
|
}
|
||||||
|
return config, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewConfig takes credentials and returns a Config object that may be further customized.
|
||||||
|
// Defaults for Alias, BaseURL, and UserAgent will be taken from respective env vars.
|
||||||
|
func NewConfig(username, password string) (Config, error) {
|
||||||
|
alias := os.Getenv("CLC_ALIAS")
|
||||||
|
agent := userAgentDefault
|
||||||
|
if v := os.Getenv("CLC_USER_AGENT"); v != "" {
|
||||||
|
agent = v
|
||||||
|
}
|
||||||
|
base := baseUriDefault
|
||||||
|
if v := os.Getenv("CLC_BASE_URL"); v != "" {
|
||||||
|
base = v
|
||||||
|
}
|
||||||
|
uri, err := url.Parse(base)
|
||||||
|
return Config{
|
||||||
|
User: User{
|
||||||
|
Username: username,
|
||||||
|
Password: password,
|
||||||
|
},
|
||||||
|
Alias: alias,
|
||||||
|
BaseURL: uri,
|
||||||
|
UserAgent: agent,
|
||||||
|
}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func FileConfig(file string) (Config, error) {
|
||||||
|
config := Config{}
|
||||||
|
b, err := ioutil.ReadFile(file)
|
||||||
|
if err != nil {
|
||||||
|
return config, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = json.Unmarshal(b, &config)
|
||||||
|
|
||||||
|
u, err := url.Parse(baseUriDefault)
|
||||||
|
config.BaseURL = u
|
||||||
|
return config, err
|
||||||
|
}
|
||||||
|
|
||||||
|
type User struct {
|
||||||
|
Username string `json:"username"`
|
||||||
|
Password string `json:"password"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Token struct {
|
||||||
|
Username string `json:"userName"`
|
||||||
|
Alias string `json:"accountAlias"`
|
||||||
|
Location string `json:"locationAlias"`
|
||||||
|
Roles []string `json:"roles"`
|
||||||
|
Token string `json:"bearerToken"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Add some real validation logic
|
||||||
|
func (t Token) Valid() bool {
|
||||||
|
return t.Token != ""
|
||||||
|
}
|
||||||
|
|
||||||
|
type Update struct {
|
||||||
|
Op string `json:"op"`
|
||||||
|
Member string `json:"member"`
|
||||||
|
Value interface{} `json:"value"`
|
||||||
|
}
|
|
@ -0,0 +1,34 @@
|
||||||
|
package api
|
||||||
|
|
||||||
|
type Link struct {
|
||||||
|
Rel string `json:"rel,omitempty"`
|
||||||
|
Href string `json:"href,omitempty"`
|
||||||
|
ID string `json:"id,omitempty"`
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
Verbs []string `json:"verbs,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Links []Link
|
||||||
|
|
||||||
|
func (l Links) GetID(rel string) (bool, string) {
|
||||||
|
if ok, v := l.GetLink(rel); ok {
|
||||||
|
return true, (*v).ID
|
||||||
|
}
|
||||||
|
return false, ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l Links) GetLink(rel string) (bool, *Link) {
|
||||||
|
for _, v := range l {
|
||||||
|
if v.Rel == rel {
|
||||||
|
return true, &v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type Customfields struct {
|
||||||
|
ID string `json:"id,omitempty"`
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
Value string `json:"value,omitempty"`
|
||||||
|
Displayvalue string `json:"displayValue,omitempty"`
|
||||||
|
}
|
|
@ -0,0 +1,49 @@
|
||||||
|
package clc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/CenturyLinkCloud/clc-sdk/aa"
|
||||||
|
"github.com/CenturyLinkCloud/clc-sdk/alert"
|
||||||
|
"github.com/CenturyLinkCloud/clc-sdk/api"
|
||||||
|
"github.com/CenturyLinkCloud/clc-sdk/dc"
|
||||||
|
"github.com/CenturyLinkCloud/clc-sdk/group"
|
||||||
|
"github.com/CenturyLinkCloud/clc-sdk/lb"
|
||||||
|
"github.com/CenturyLinkCloud/clc-sdk/server"
|
||||||
|
"github.com/CenturyLinkCloud/clc-sdk/status"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Client struct {
|
||||||
|
client *api.Client
|
||||||
|
|
||||||
|
Server *server.Service
|
||||||
|
Status *status.Service
|
||||||
|
AA *aa.Service
|
||||||
|
Alert *alert.Service
|
||||||
|
LB *lb.Service
|
||||||
|
Group *group.Service
|
||||||
|
DC *dc.Service
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(config api.Config) *Client {
|
||||||
|
c := &Client{
|
||||||
|
client: api.New(config),
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Server = server.New(c.client)
|
||||||
|
c.Status = status.New(c.client)
|
||||||
|
c.AA = aa.New(c.client)
|
||||||
|
c.Alert = alert.New(c.client)
|
||||||
|
c.LB = lb.New(c.client)
|
||||||
|
c.Group = group.New(c.client)
|
||||||
|
c.DC = dc.New(c.client)
|
||||||
|
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) Alias(alias string) *Client {
|
||||||
|
c.client.Config().Alias = alias
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) Authenticate() error {
|
||||||
|
return c.client.Auth()
|
||||||
|
}
|
|
@ -0,0 +1,27 @@
|
||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
# Automatic checks
|
||||||
|
test -z "$(gofmt -l -w . | tee /dev/stderr)"
|
||||||
|
test -z "$(goimports -l -w . | tee /dev/stderr)"
|
||||||
|
godep go test -race ./...
|
||||||
|
|
||||||
|
# Run test coverage on each subdirectories and merge the coverage profile.
|
||||||
|
|
||||||
|
echo "mode: count" > profile.cov
|
||||||
|
|
||||||
|
# Standard go tooling behavior is to ignore dirs with leading underscors
|
||||||
|
for dir in $(find . -maxdepth 10 -not -path './.git*' -not -path '*/_*' -type d);
|
||||||
|
do
|
||||||
|
if ls $dir/*.go &> /dev/null; then
|
||||||
|
godep go test -covermode=count -coverprofile=$dir/profile.tmp $dir
|
||||||
|
if [ -f $dir/profile.tmp ]
|
||||||
|
then
|
||||||
|
cat $dir/profile.tmp | tail -n +2 >> profile.cov
|
||||||
|
rm $dir/profile.tmp
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
go tool cover -func profile.cov
|
||||||
|
|
||||||
|
goveralls -coverprofile=profile.cov -service=travis-ci -repotoken $COVERALLS_REPO_TOKEN
|
|
@ -0,0 +1,65 @@
|
||||||
|
package dc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/CenturyLinkCloud/clc-sdk/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
func New(client api.HTTP) *Service {
|
||||||
|
return &Service{
|
||||||
|
client: client,
|
||||||
|
config: client.Config(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Service struct {
|
||||||
|
client api.HTTP
|
||||||
|
config *api.Config
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) Get(id string) (*Response, error) {
|
||||||
|
url := fmt.Sprintf("%s/datacenters/%s/%s?groupLinks=true", s.config.BaseURL, s.config.Alias, id)
|
||||||
|
dc := &Response{}
|
||||||
|
err := s.client.Get(url, dc)
|
||||||
|
return dc, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) GetAll() ([]*Response, error) {
|
||||||
|
url := fmt.Sprintf("%s/datacenters/%s", s.config.BaseURL, s.config.Alias)
|
||||||
|
dcs := make([]*Response, 0)
|
||||||
|
err := s.client.Get(url, &dcs)
|
||||||
|
return dcs, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) GetCapabilities(id string) (*CapabilitiesResponse, error) {
|
||||||
|
url := fmt.Sprintf("%s/datacenters/%s/%s/deploymentCapabilities", s.config.BaseURL, s.config.Alias, id)
|
||||||
|
c := &CapabilitiesResponse{}
|
||||||
|
err := s.client.Get(url, c)
|
||||||
|
return c, err
|
||||||
|
}
|
||||||
|
|
||||||
|
type Response struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Links api.Links `json:"links"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CapabilitiesResponse struct {
|
||||||
|
SupportsPremiumStorage bool `json:"supportsPremiumStorage"`
|
||||||
|
SupportsBareMetalServers bool `json:"supportsBareMetalServers"`
|
||||||
|
SupportsSharedLoadBalancer bool `json:"supportsSharedLoadBalancer"`
|
||||||
|
Templates []struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
StorageSizeGB string `json:"storageSizeGB"`
|
||||||
|
Capabilities []string `json:"capabilities"`
|
||||||
|
ReservedDrivePaths []string `json:"reservedDrivePaths"`
|
||||||
|
} `json:"templates"`
|
||||||
|
DeployableNetworks []struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
NetworkId string `json:"networkId"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
AccountID string `json:"accountID"`
|
||||||
|
} `json:deployableNetworks`
|
||||||
|
}
|
|
@ -0,0 +1,187 @@
|
||||||
|
package group
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/CenturyLinkCloud/clc-sdk/api"
|
||||||
|
"github.com/CenturyLinkCloud/clc-sdk/status"
|
||||||
|
)
|
||||||
|
|
||||||
|
func New(client api.HTTP) *Service {
|
||||||
|
return &Service{
|
||||||
|
client: client,
|
||||||
|
config: client.Config(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Service struct {
|
||||||
|
client api.HTTP
|
||||||
|
config *api.Config
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) Get(id string) (*Response, error) {
|
||||||
|
url := fmt.Sprintf("%s/groups/%s/%s", s.config.BaseURL, s.config.Alias, id)
|
||||||
|
resp := &Response{}
|
||||||
|
err := s.client.Get(url, resp)
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) Create(group Group) (*Response, error) {
|
||||||
|
resp := &Response{}
|
||||||
|
url := fmt.Sprintf("%s/groups/%s", s.config.BaseURL, s.config.Alias)
|
||||||
|
err := s.client.Post(url, group, resp)
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) Update(id string, updates ...api.Update) error {
|
||||||
|
url := fmt.Sprintf("%s/groups/%s/%s", s.config.BaseURL, s.config.Alias, id)
|
||||||
|
return s.client.Patch(url, updates, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) Delete(id string) (*status.Status, error) {
|
||||||
|
url := fmt.Sprintf("%s/groups/%s/%s", s.config.BaseURL, s.config.Alias, id)
|
||||||
|
resp := &status.Status{}
|
||||||
|
err := s.client.Delete(url, resp)
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) Archive(id string) (*status.Status, error) {
|
||||||
|
url := fmt.Sprintf("%s/groups/%s/%s/archive", s.config.BaseURL, s.config.Alias, id)
|
||||||
|
resp := &status.Status{}
|
||||||
|
err := s.client.Post(url, "", resp)
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) Restore(id, intoGroup string) (*status.QueuedResponse, error) {
|
||||||
|
url := fmt.Sprintf("%s/groups/%s/%s/restore", s.config.BaseURL, s.config.Alias, id)
|
||||||
|
resp := &status.QueuedResponse{}
|
||||||
|
body := fmt.Sprintf(`{"targetGroupId": "%v"}`, intoGroup)
|
||||||
|
err := s.client.Post(url, body, resp)
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) SetDefaults(id string, defaults *GroupDefaults) error {
|
||||||
|
url := fmt.Sprintf("%s/groups/%s/%s/defaults", s.config.BaseURL, s.config.Alias, id)
|
||||||
|
var resp interface{}
|
||||||
|
err := s.client.Post(url, defaults, resp)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) SetHorizontalAutoscalePolicy(id string, policy *HorizontalAutoscalePolicy) (*interface{}, error) {
|
||||||
|
url := fmt.Sprintf("%s/groups/%s/%s/horizontalAutoscalePolicy", s.config.BaseURL, s.config.Alias, id)
|
||||||
|
var resp interface{}
|
||||||
|
err := s.client.Put(url, policy, resp)
|
||||||
|
return &resp, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func UpdateName(name string) api.Update {
|
||||||
|
return api.Update{
|
||||||
|
Op: "set",
|
||||||
|
Member: "name",
|
||||||
|
Value: name,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func UpdateDescription(desc string) api.Update {
|
||||||
|
return api.Update{
|
||||||
|
Op: "set",
|
||||||
|
Member: "description",
|
||||||
|
Value: desc,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func UpdateParentGroupID(id string) api.Update {
|
||||||
|
return api.Update{
|
||||||
|
Op: "set",
|
||||||
|
Member: "parentGroupId",
|
||||||
|
Value: id,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func UpdateCustomfields(fields []api.Customfields) api.Update {
|
||||||
|
return api.Update{
|
||||||
|
Op: "set",
|
||||||
|
Member: "customFields",
|
||||||
|
Value: fields,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// request body for creating groups
|
||||||
|
type Group struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Description string `json:"description,omitempty"`
|
||||||
|
ParentGroupID string `json:"parentGroupId"`
|
||||||
|
CustomFields []api.Customfields `json:"customFields,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// response body for group get
|
||||||
|
type Response struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
Locationid string `json:"locationId"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Links api.Links `json:"links"`
|
||||||
|
Groups []Groups `json:"groups"`
|
||||||
|
Changeinfo struct {
|
||||||
|
Createddate time.Time `json:"createdDate"`
|
||||||
|
Createdby string `json:"createdBy"`
|
||||||
|
Modifieddate time.Time `json:"modifiedDate"`
|
||||||
|
Modifiedby string `json:"modifiedBy"`
|
||||||
|
} `json:"changeInfo"`
|
||||||
|
Customfields []api.Customfields `json:"customFields"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Response) ParentGroupID() string {
|
||||||
|
if ok, link := r.Links.GetLink("parentGroup"); ok {
|
||||||
|
return link.ID
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Response) Servers() []string {
|
||||||
|
ids := make([]string, 0)
|
||||||
|
for _, l := range r.Links {
|
||||||
|
if l.Rel == "server" {
|
||||||
|
ids = append(ids, l.ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ids
|
||||||
|
}
|
||||||
|
|
||||||
|
// nested groups under response
|
||||||
|
type Groups struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
Locationid string `json:"locationId"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Serverscount int `json:"serversCount"`
|
||||||
|
Groups []Groups `json:"groups"`
|
||||||
|
Links api.Links `json:"links"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// request body for /v2/groups/ALIAS/ID/defaults
|
||||||
|
type GroupDefaults struct {
|
||||||
|
CPU string `json:"cpu,omitempty"`
|
||||||
|
MemoryGB string `json:"memoryGB,omitempty"`
|
||||||
|
NetworkID string `json:"networkId,omitempty"`
|
||||||
|
primaryDns string `json:"primaryDns,omitempty"`
|
||||||
|
secondaryDns string `json:"secondaryDns,omitempty"`
|
||||||
|
templateName string `json:"templateName,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// request body for /v2/groups/ALIAS/ID/horizontalAutoscalePolicy
|
||||||
|
type HorizontalAutoscalePolicy struct {
|
||||||
|
PolicyId string `json:"policyId,omitempty"`
|
||||||
|
LoadBalancerPool []PoolPolicy `json:"loadBalancerPool,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PoolPolicy struct {
|
||||||
|
ID string `json:"id,omitempty"`
|
||||||
|
PrivatePort int `json:"privatePort,omitempty"`
|
||||||
|
PublicPort int `json:"publicPort,omitempty"`
|
||||||
|
}
|
|
@ -0,0 +1,135 @@
|
||||||
|
package lb
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/CenturyLinkCloud/clc-sdk/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
func New(client api.HTTP) *Service {
|
||||||
|
return &Service{
|
||||||
|
client: client,
|
||||||
|
config: client.Config(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Service struct {
|
||||||
|
client api.HTTP
|
||||||
|
config *api.Config
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) Get(dc, id string) (*LoadBalancer, error) {
|
||||||
|
url := fmt.Sprintf("%s/sharedLoadBalancers/%s/%s/%s", s.config.BaseURL, s.config.Alias, dc, id)
|
||||||
|
resp := &LoadBalancer{}
|
||||||
|
err := s.client.Get(url, resp)
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) GetAll(dc string) ([]*LoadBalancer, error) {
|
||||||
|
url := fmt.Sprintf("%s/sharedLoadBalancers/%s/%s", s.config.BaseURL, s.config.Alias, dc)
|
||||||
|
resp := make([]*LoadBalancer, 0)
|
||||||
|
err := s.client.Get(url, &resp)
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) Create(dc string, lb LoadBalancer) (*LoadBalancer, error) {
|
||||||
|
url := fmt.Sprintf("%s/sharedLoadBalancers/%s/%s", s.config.BaseURL, s.config.Alias, dc)
|
||||||
|
resp := &LoadBalancer{}
|
||||||
|
err := s.client.Post(url, lb, resp)
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) Update(dc, id string, lb LoadBalancer) error {
|
||||||
|
url := fmt.Sprintf("%s/sharedLoadBalancers/%s/%s/%s", s.config.BaseURL, s.config.Alias, dc, id)
|
||||||
|
err := s.client.Put(url, lb, nil)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) Delete(dc, id string) error {
|
||||||
|
url := fmt.Sprintf("%s/sharedLoadBalancers/%s/%s/%s", s.config.BaseURL, s.config.Alias, dc, id)
|
||||||
|
return s.client.Delete(url, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) GetPool(dc, lb, pool string) (*Pool, error) {
|
||||||
|
url := fmt.Sprintf("%s/sharedLoadBalancers/%s/%s/%s/pools/%s", s.config.BaseURL, s.config.Alias, dc, lb, pool)
|
||||||
|
resp := &Pool{}
|
||||||
|
err := s.client.Get(url, resp)
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) GetAllPools(dc, lb string) ([]*Pool, error) {
|
||||||
|
url := fmt.Sprintf("%s/sharedLoadBalancers/%s/%s/%s/pools", s.config.BaseURL, s.config.Alias, dc, lb)
|
||||||
|
resp := make([]*Pool, 0)
|
||||||
|
err := s.client.Get(url, &resp)
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) CreatePool(dc, lb string, pool Pool) (*Pool, error) {
|
||||||
|
url := fmt.Sprintf("%s/sharedLoadBalancers/%s/%s/%s/pools", s.config.BaseURL, s.config.Alias, dc, lb)
|
||||||
|
resp := &Pool{}
|
||||||
|
err := s.client.Post(url, pool, resp)
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) UpdatePool(dc, lb, id string, pool Pool) error {
|
||||||
|
url := fmt.Sprintf("%s/sharedLoadBalancers/%s/%s/%s/pools/%s", s.config.BaseURL, s.config.Alias, dc, lb, id)
|
||||||
|
err := s.client.Put(url, pool, nil)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) DeletePool(dc, lb, pool string) error {
|
||||||
|
url := fmt.Sprintf("%s/sharedLoadBalancers/%s/%s/%s/pools/%s", s.config.BaseURL, s.config.Alias, dc, lb, pool)
|
||||||
|
return s.client.Delete(url, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) GetAllNodes(dc, lb, pool string) ([]*Node, error) {
|
||||||
|
url := fmt.Sprintf("%s/sharedLoadBalancers/%s/%s/%s/pools/%s/nodes", s.config.BaseURL, s.config.Alias, dc, lb, pool)
|
||||||
|
resp := make([]*Node, 0)
|
||||||
|
err := s.client.Get(url, &resp)
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) UpdateNodes(dc, lb, pool string, nodes ...Node) error {
|
||||||
|
url := fmt.Sprintf("%s/sharedLoadBalancers/%s/%s/%s/pools/%s/nodes", s.config.BaseURL, s.config.Alias, dc, lb, pool)
|
||||||
|
err := s.client.Put(url, nodes, nil)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
type LoadBalancer struct {
|
||||||
|
ID string `json:"id,omitempty"`
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
Description string `json:"description,omitempty"`
|
||||||
|
IPaddress string `json:"ipAddress,omitempty"`
|
||||||
|
Status string `json:"status,omitempty"`
|
||||||
|
Pools []Pool `json:"pools,omitempty"`
|
||||||
|
Links api.Links `json:"links,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Pool struct {
|
||||||
|
ID string `json:"id,omitempty"`
|
||||||
|
Port int `json:"port,omitempty"`
|
||||||
|
Method Method `json:"method"`
|
||||||
|
Persistence Persistence `json:"persistence"`
|
||||||
|
Nodes []Node `json:"nodes,omitempty"`
|
||||||
|
Links api.Links `json:"links,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Node struct {
|
||||||
|
Status string `json:"status,omitempty"`
|
||||||
|
IPaddress string `json:"ipAddress"`
|
||||||
|
PrivatePort int `json:"privatePort"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Persistence string
|
||||||
|
|
||||||
|
const (
|
||||||
|
Standard Persistence = "standard"
|
||||||
|
Sticky Persistence = "sticky"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Method string
|
||||||
|
|
||||||
|
const (
|
||||||
|
LeastConn Method = "leastConnection"
|
||||||
|
RoundRobin Method = "roundRobin"
|
||||||
|
)
|
|
@ -0,0 +1,388 @@
|
||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
|
||||||
|
"github.com/CenturyLinkCloud/clc-sdk/api"
|
||||||
|
"github.com/CenturyLinkCloud/clc-sdk/status"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrInvalidServer = fmt.Errorf("server: server missing required field(s). (Name, CPU, MemoryGB, GroupID, SourceServerID, Type)")
|
||||||
|
)
|
||||||
|
|
||||||
|
func New(client api.HTTP) *Service {
|
||||||
|
return &Service{
|
||||||
|
client: client,
|
||||||
|
config: client.Config(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Service struct {
|
||||||
|
client api.HTTP
|
||||||
|
config *api.Config
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) Get(name string) (*Response, error) {
|
||||||
|
url := fmt.Sprintf("%s/servers/%s/%s", s.config.BaseURL, s.config.Alias, name)
|
||||||
|
if regexp.MustCompile("^[0-9a-f]{32}$").MatchString(name) {
|
||||||
|
url = fmt.Sprintf("%s?uuid=true", url)
|
||||||
|
}
|
||||||
|
resp := &Response{}
|
||||||
|
err := s.client.Get(url, resp)
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) Create(server Server) (*status.QueuedResponse, error) {
|
||||||
|
if !server.Valid() {
|
||||||
|
return nil, ErrInvalidServer
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := &status.QueuedResponse{}
|
||||||
|
url := fmt.Sprintf("%s/servers/%s", s.config.BaseURL, s.config.Alias)
|
||||||
|
err := s.client.Post(url, server, resp)
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) Update(name string, updates ...api.Update) (*status.Status, error) {
|
||||||
|
resp := &status.Status{}
|
||||||
|
url := fmt.Sprintf("%s/servers/%s/%s", s.config.BaseURL, s.config.Alias, name)
|
||||||
|
err := s.client.Patch(url, updates, resp)
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) Edit(name string, updates ...api.Update) error {
|
||||||
|
url := fmt.Sprintf("%s/servers/%s/%s", s.config.BaseURL, s.config.Alias, name)
|
||||||
|
err := s.client.Patch(url, updates, nil)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) Delete(name string) (*status.QueuedResponse, error) {
|
||||||
|
url := fmt.Sprintf("%s/servers/%s/%s", s.config.BaseURL, s.config.Alias, name)
|
||||||
|
resp := &status.QueuedResponse{}
|
||||||
|
err := s.client.Delete(url, resp)
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) GetCredentials(name string) (Credentials, error) {
|
||||||
|
url := fmt.Sprintf("%s/servers/%s/%s/credentials", s.config.BaseURL, s.config.Alias, name)
|
||||||
|
resp := Credentials{}
|
||||||
|
err := s.client.Get(url, &resp)
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
|
||||||
|
type Credentials struct {
|
||||||
|
Username string `json:"userName"`
|
||||||
|
Password string `json:"password"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) Archive(servers ...string) ([]*status.QueuedResponse, error) {
|
||||||
|
url := fmt.Sprintf("%s/operations/%s/servers/archive", s.config.BaseURL, s.config.Alias)
|
||||||
|
var resp []*status.QueuedResponse
|
||||||
|
err := s.client.Post(url, servers, &resp)
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) Restore(name, group string) (*status.Status, error) {
|
||||||
|
restore := map[string]string{"targetGroupId": group}
|
||||||
|
url := fmt.Sprintf("%s/servers/%s/%s/restore", s.config.BaseURL, s.config.Alias, name)
|
||||||
|
resp := &status.Status{}
|
||||||
|
err := s.client.Post(url, restore, resp)
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) CreateSnapshot(expiration int, servers ...string) ([]*status.QueuedResponse, error) {
|
||||||
|
snapshot := Snapshot{Expiration: expiration, Servers: servers}
|
||||||
|
url := fmt.Sprintf("%s/operations/%s/servers/createSnapshot", s.config.BaseURL, s.config.Alias)
|
||||||
|
var resp []*status.QueuedResponse
|
||||||
|
err := s.client.Post(url, snapshot, &resp)
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) DeleteSnapshot(server, id string) (*status.Status, error) {
|
||||||
|
url := fmt.Sprintf("%s/servers/%s/%s/snapshots/%s", s.config.BaseURL, s.config.Alias, server, id)
|
||||||
|
resp := &status.Status{}
|
||||||
|
err := s.client.Delete(url, resp)
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) RevertSnapshot(server, id string) (*status.Status, error) {
|
||||||
|
url := fmt.Sprintf("%s/servers/%s/%s/snapshots/%s/restore", s.config.BaseURL, s.config.Alias, server, id)
|
||||||
|
resp := &status.Status{}
|
||||||
|
err := s.client.Post(url, nil, resp)
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
|
||||||
|
type Snapshot struct {
|
||||||
|
Expiration int `json:"snapshotExpirationDays"`
|
||||||
|
Servers []string `json:"serverIds"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) ExecutePackage(pkg Package, servers ...string) ([]*status.QueuedResponse, error) {
|
||||||
|
url := fmt.Sprintf("%s/operations/%s/servers/executePackage", s.config.BaseURL, s.config.Alias)
|
||||||
|
var resp []*status.QueuedResponse
|
||||||
|
exec := executePackage{Servers: servers, Package: pkg}
|
||||||
|
err := s.client.Post(url, exec, &resp)
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
|
||||||
|
type executePackage struct {
|
||||||
|
Servers []string `json:"servers"`
|
||||||
|
Package Package `json:"package"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Package struct {
|
||||||
|
ID string `json:"packageId"`
|
||||||
|
Params map[string]string `json:"parameters"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) PowerState(state PowerState, servers ...string) ([]*status.QueuedResponse, error) {
|
||||||
|
url := fmt.Sprintf("%s/operations/%s/servers/%s", s.config.BaseURL, s.config.Alias, state)
|
||||||
|
var resp []*status.QueuedResponse
|
||||||
|
err := s.client.Post(url, servers, &resp)
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
|
||||||
|
type PowerState int
|
||||||
|
|
||||||
|
const (
|
||||||
|
On = iota
|
||||||
|
Off
|
||||||
|
Pause
|
||||||
|
Reboot
|
||||||
|
Reset
|
||||||
|
ShutDown
|
||||||
|
StartMaintenance
|
||||||
|
StopMaintenance
|
||||||
|
)
|
||||||
|
|
||||||
|
func (p PowerState) String() string {
|
||||||
|
switch p {
|
||||||
|
case On:
|
||||||
|
return "powerOn"
|
||||||
|
case Off:
|
||||||
|
return "powerOff"
|
||||||
|
case Pause:
|
||||||
|
return "pause"
|
||||||
|
case Reboot:
|
||||||
|
return "reboot"
|
||||||
|
case Reset:
|
||||||
|
return "reset"
|
||||||
|
case ShutDown:
|
||||||
|
return "shutDown"
|
||||||
|
case StartMaintenance:
|
||||||
|
return "startMaintenance"
|
||||||
|
case StopMaintenance:
|
||||||
|
return "stopMaintenance"
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) GetPublicIP(name string, ip string) (*PublicIP, error) {
|
||||||
|
url := fmt.Sprintf("%s/servers/%s/%s/publicIPAddresses/%s", s.config.BaseURL, s.config.Alias, name, ip)
|
||||||
|
resp := &PublicIP{}
|
||||||
|
err := s.client.Get(url, resp)
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) AddPublicIP(name string, ip PublicIP) (*status.Status, error) {
|
||||||
|
url := fmt.Sprintf("%s/servers/%s/%s/publicIPAddresses", s.config.BaseURL, s.config.Alias, name)
|
||||||
|
resp := &status.Status{}
|
||||||
|
err := s.client.Post(url, ip, resp)
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) UpdatePublicIP(name string, public string, ip PublicIP) (*status.Status, error) {
|
||||||
|
url := fmt.Sprintf("%s/servers/%s/%s/publicIPAddresses/%s", s.config.BaseURL, s.config.Alias, name, public)
|
||||||
|
resp := &status.Status{}
|
||||||
|
err := s.client.Put(url, ip, resp)
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) DeletePublicIP(name, ip string) (*status.Status, error) {
|
||||||
|
url := fmt.Sprintf("%s/servers/%s/%s/publicIPAddresses/%s", s.config.BaseURL, s.config.Alias, name, ip)
|
||||||
|
resp := &status.Status{}
|
||||||
|
err := s.client.Delete(url, resp)
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
|
||||||
|
type PublicIP struct {
|
||||||
|
InternalIP string `json:"internalIPAddress,omitempty"`
|
||||||
|
Ports []Port `json:"ports,omitempty"`
|
||||||
|
SourceRestrictions []SourceRestriction `json:"sourceRestrictions,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Port struct {
|
||||||
|
Protocol string `json:"protocol"`
|
||||||
|
Port int `json:"port"`
|
||||||
|
PortTo int `json:"portTo,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SourceRestriction struct {
|
||||||
|
CIDR string `json:"cidr"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Disk struct {
|
||||||
|
DiskID string `json:"diskId,omitempty"`
|
||||||
|
Path string `json:"path,omitempty"`
|
||||||
|
SizeGB int `json:"sizeGB,omitempty"`
|
||||||
|
Type string `json:"type,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) AddSecondaryNetwork(name, networkId, ip string) (*status.Status, error) {
|
||||||
|
url := fmt.Sprintf("%s/servers/%s/%s/networks", s.config.BaseURL, s.config.Alias, name)
|
||||||
|
req := &SecondaryNetwork{
|
||||||
|
NetworkID: networkId,
|
||||||
|
IPAddress: ip,
|
||||||
|
}
|
||||||
|
// returned a non-standard status object, repackage into a proper one
|
||||||
|
resp := &status.QueuedOperation{}
|
||||||
|
err := s.client.Post(url, req, resp)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return resp.Status(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type SecondaryNetwork struct {
|
||||||
|
NetworkID string `json:"networkId,omitempty"`
|
||||||
|
IPAddress string `json:"ipAddress,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func UpdateCPU(num int) api.Update {
|
||||||
|
return api.Update{
|
||||||
|
Op: "set",
|
||||||
|
Member: "cpu",
|
||||||
|
Value: num,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func UpdateMemory(num int) api.Update {
|
||||||
|
return api.Update{
|
||||||
|
Op: "set",
|
||||||
|
Member: "memory",
|
||||||
|
Value: num,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func UpdateCredentials(current, updated string) api.Update {
|
||||||
|
return api.Update{
|
||||||
|
Op: "set",
|
||||||
|
Member: "password",
|
||||||
|
Value: struct {
|
||||||
|
Current string `json:"current"`
|
||||||
|
Password string `json:"password"`
|
||||||
|
}{
|
||||||
|
current,
|
||||||
|
updated,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func UpdateGroup(group string) api.Update {
|
||||||
|
return api.Update{
|
||||||
|
Op: "set",
|
||||||
|
Member: "groupId",
|
||||||
|
Value: group,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func UpdateDescription(desc string) api.Update {
|
||||||
|
return api.Update{
|
||||||
|
Op: "set",
|
||||||
|
Member: "description",
|
||||||
|
Value: desc,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func UpdateAdditionaldisks(disks []Disk) api.Update {
|
||||||
|
return api.Update{
|
||||||
|
Op: "set",
|
||||||
|
Member: "disks",
|
||||||
|
Value: disks,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func UpdateCustomfields(fields []api.Customfields) api.Update {
|
||||||
|
return api.Update{
|
||||||
|
Op: "set",
|
||||||
|
Member: "customFields",
|
||||||
|
Value: fields,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Server struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Description string `json:"description,omitempty"`
|
||||||
|
GroupID string `json:"groupId"`
|
||||||
|
SourceServerID string `json:"sourceServerId"`
|
||||||
|
IsManagedOS bool `json:"isManagedOS,omitempty"`
|
||||||
|
PrimaryDNS string `json:"primaryDns,omitempty"`
|
||||||
|
SecondaryDNS string `json:"secondaryDns,omitempty"`
|
||||||
|
NetworkID string `json:"networkId,omitempty"`
|
||||||
|
IPaddress string `json:"ipAddress,omitempty"`
|
||||||
|
Password string `json:"password,omitempty"`
|
||||||
|
CPU int `json:"cpu"`
|
||||||
|
MemoryGB int `json:"memoryGB"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Storagetype string `json:"storageType,omitempty"`
|
||||||
|
Customfields []api.Customfields `json:"customFields,omitempty"`
|
||||||
|
Additionaldisks []Disk `json:"additionalDisks,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) Valid() bool {
|
||||||
|
return s.Name != "" && s.CPU != 0 && s.MemoryGB != 0 && s.GroupID != "" && s.SourceServerID != ""
|
||||||
|
}
|
||||||
|
|
||||||
|
type Response struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
GroupID string `json:"groupId"`
|
||||||
|
IsTemplate bool `json:"isTemplate"`
|
||||||
|
LocationID string `json:"locationId"`
|
||||||
|
OStype string `json:"osType"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Details struct {
|
||||||
|
IPaddresses []struct {
|
||||||
|
Internal string `json:"internal"`
|
||||||
|
Public string `json:"public"`
|
||||||
|
} `json:"ipAddresses"`
|
||||||
|
AlertPolicies []struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Links api.Links `json:"links"`
|
||||||
|
} `json:"alertPolicies"`
|
||||||
|
CPU int `json:"cpu"`
|
||||||
|
Diskcount int `json:"diskCount"`
|
||||||
|
Hostname string `json:"hostName"`
|
||||||
|
InMaintenanceMode bool `json:"inMaintenanceMode"`
|
||||||
|
MemoryMB int `json:"memoryMB"`
|
||||||
|
Powerstate string `json:"powerState"`
|
||||||
|
Storagegb int `json:"storageGB"`
|
||||||
|
Disks []struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
SizeGB int `json:"sizeGB"`
|
||||||
|
PartitionPaths []interface{} `json:"partitionPaths"`
|
||||||
|
} `json:"disks"`
|
||||||
|
Partitions []struct {
|
||||||
|
SizeGB float64 `json:"sizeGB"`
|
||||||
|
Path string `json:"path"`
|
||||||
|
} `json:"partitions"`
|
||||||
|
Snapshots []struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Links api.Links `json:"links"`
|
||||||
|
} `json:"snapshots"`
|
||||||
|
Customfields []api.Customfields `json:"customFields,omitempty"`
|
||||||
|
} `json:"details"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Storagetype string `json:"storageType"`
|
||||||
|
ChangeInfo struct {
|
||||||
|
CreatedDate string `json:"createdDate"`
|
||||||
|
CreatedBy string `json:"createdBy"`
|
||||||
|
ModifiedDate string `json:"modifiedDate"`
|
||||||
|
ModifiedBy string `json:"modifiedBy"`
|
||||||
|
} `json:"changeInfo"`
|
||||||
|
Links api.Links `json:"links"`
|
||||||
|
}
|
|
@ -0,0 +1,130 @@
|
||||||
|
package status
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/CenturyLinkCloud/clc-sdk/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
func New(client api.HTTP) *Service {
|
||||||
|
return &Service{
|
||||||
|
client: client,
|
||||||
|
config: client.Config(),
|
||||||
|
PollInterval: 30 * time.Second,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Service struct {
|
||||||
|
client api.HTTP
|
||||||
|
config *api.Config
|
||||||
|
|
||||||
|
PollInterval time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) Get(id string) (*Response, error) {
|
||||||
|
url := fmt.Sprintf("%s/operations/%s/status/%s", s.config.BaseURL, s.config.Alias, id)
|
||||||
|
status := &Response{}
|
||||||
|
err := s.client.Get(url, status)
|
||||||
|
return status, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) Poll(id string, poll chan *Response) error {
|
||||||
|
for {
|
||||||
|
status, err := s.Get(id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !status.Running() {
|
||||||
|
poll <- status
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
time.Sleep(s.PollInterval)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Status struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Rel string `json:"rel"`
|
||||||
|
Href string `json:"href"`
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Response represents a running async job
|
||||||
|
result from polling status
|
||||||
|
{"status": "succeeded"}
|
||||||
|
*/
|
||||||
|
type Response struct {
|
||||||
|
Status string `json:"status"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Response) Complete() bool {
|
||||||
|
return s.Status == Complete
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Response) Failed() bool {
|
||||||
|
return s.Status == Failed
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Response) Running() bool {
|
||||||
|
return !s.Complete() && !s.Failed() && s.Status != ""
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
Complete = "succeeded"
|
||||||
|
Failed = "failed"
|
||||||
|
)
|
||||||
|
|
||||||
|
/* QueuedResponse represents a returned response for an async platform job
|
||||||
|
eg. create server
|
||||||
|
{"server":"web", "isQueued":true, "links":[
|
||||||
|
{"rel":"status", "href":"...", "id":"wa1-12345"},
|
||||||
|
{"rel":"self", "href":"...", "id":"8134c91a66784c6dada651eba90a5123"}]}
|
||||||
|
*/
|
||||||
|
type QueuedResponse struct {
|
||||||
|
Server string `json:"server,omitempty"`
|
||||||
|
IsQueued bool `json:"isQueued,omitempty"`
|
||||||
|
Links api.Links `json:"links,omitempty"`
|
||||||
|
Error string `json:"errorMessage,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *QueuedResponse) GetStatusID() (bool, string) {
|
||||||
|
return q.Links.GetID("status")
|
||||||
|
}
|
||||||
|
|
||||||
|
/* QueuedOperation may be a one-off and/or experimental version of QueuedResponse
|
||||||
|
eg. add secondary network
|
||||||
|
{"operationId": "2b70710dba4142dcaf3ab2de68e4f40c", "uri": "..."}
|
||||||
|
*/
|
||||||
|
type QueuedOperation struct {
|
||||||
|
OperationID string `json:"operationId,omitempty"`
|
||||||
|
URI string `json:"uri,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *QueuedOperation) GetStatusID() (bool, string) {
|
||||||
|
return q.OperationID != "", q.OperationID
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *QueuedOperation) GetHref() (bool, string) {
|
||||||
|
var path = ""
|
||||||
|
if q.URI != "" {
|
||||||
|
u, err := url.Parse(q.URI)
|
||||||
|
if err == nil {
|
||||||
|
path = u.Path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return path != "", path
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *QueuedOperation) Status() *Status {
|
||||||
|
st := &Status{}
|
||||||
|
if ok, id := q.GetStatusID(); ok {
|
||||||
|
st.ID = id
|
||||||
|
}
|
||||||
|
if ok, href := q.GetHref(); ok {
|
||||||
|
st.Href = href
|
||||||
|
}
|
||||||
|
return st
|
||||||
|
}
|
|
@ -11,6 +11,7 @@ body.layout-aws,
|
||||||
body.layout-azure,
|
body.layout-azure,
|
||||||
body.layout-chef,
|
body.layout-chef,
|
||||||
body.layout-azurerm,
|
body.layout-azurerm,
|
||||||
|
body.layout-clc,
|
||||||
body.layout-cloudflare,
|
body.layout-cloudflare,
|
||||||
body.layout-cloudstack,
|
body.layout-cloudstack,
|
||||||
body.layout-consul,
|
body.layout-consul,
|
||||||
|
|
|
@ -0,0 +1,59 @@
|
||||||
|
---
|
||||||
|
layout: "clc"
|
||||||
|
page_title: "Provider: CenturyLinkCloud"
|
||||||
|
sidebar_current: "docs-clc-index"
|
||||||
|
description: |-
|
||||||
|
The CenturyLinkCloud provider is used to interact with the many resources
|
||||||
|
supported by CLC. The provider needs to be configured with account
|
||||||
|
credentials before it can be used.
|
||||||
|
---
|
||||||
|
|
||||||
|
# CLC Provider
|
||||||
|
|
||||||
|
The clc provider is used to interact with the many resources supported
|
||||||
|
by CenturyLinkCloud. The provider needs to be configured with account
|
||||||
|
credentials before it can be used.
|
||||||
|
|
||||||
|
Use the navigation to the left to read about the available resources.
|
||||||
|
|
||||||
|
For additional documentation, see the [CLC Developer Center](https://www.ctl.io/developers/)
|
||||||
|
|
||||||
|
## Example Usage
|
||||||
|
|
||||||
|
```
|
||||||
|
# Configure the CLC Provider
|
||||||
|
provider "clc" {
|
||||||
|
username = "${var.clc_username}"
|
||||||
|
password = "${var.clc_password}"
|
||||||
|
account = "${var.clc_account}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Create a server
|
||||||
|
resource "clc_server" "node" {
|
||||||
|
...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Account Bootstrap
|
||||||
|
|
||||||
|
Trial accounts are available by signing up on the control portal [https://control.ctl.io](https://control.ctl.io).
|
||||||
|
|
||||||
|
For new accounts, you should initially run these steps manually:
|
||||||
|
|
||||||
|
- [Create a network.](https://control.ctl.io/Network/network)
|
||||||
|
- [Provision a server.](https://control.ctl.io/create)
|
||||||
|
|
||||||
|
|
||||||
|
## Argument Reference
|
||||||
|
|
||||||
|
The following arguments are supported:
|
||||||
|
|
||||||
|
* `clc_username` - (Required) This is the CLC account username. It must be provided, but
|
||||||
|
it can also be sourced from the `CLC_USERNAME` environment variable.
|
||||||
|
|
||||||
|
* `clc_password` - (Required) This is the CLC account password. It must be provided, but
|
||||||
|
it can also be sourced from the `CLC_PASSWORD` environment variable.
|
||||||
|
|
||||||
|
* `clc_account` - (Required) This is the CLC account alias. It must be provided, but
|
||||||
|
it can also be sourced from the `CLC_ACCOUNT` environment variable.
|
|
@ -0,0 +1,55 @@
|
||||||
|
---
|
||||||
|
layout: "clc"
|
||||||
|
page_title: "clc: clc_group"
|
||||||
|
sidebar_current: "docs-clc-resource-group"
|
||||||
|
description: |-
|
||||||
|
Manages a CLC server group.
|
||||||
|
---
|
||||||
|
|
||||||
|
# clc\_group
|
||||||
|
|
||||||
|
Manages a CLC server group. Either provisions or resolves to an existing group.
|
||||||
|
|
||||||
|
See also [Complete API documentation](https://www.ctl.io/api-docs/v2/#groups).
|
||||||
|
|
||||||
|
## Example Usage
|
||||||
|
|
||||||
|
```
|
||||||
|
# Provision/Resolve a server group
|
||||||
|
resource "clc_group" "frontends" {
|
||||||
|
location_id = "WA1"
|
||||||
|
name = "frontends"
|
||||||
|
parent = "Default Group"
|
||||||
|
}
|
||||||
|
|
||||||
|
output "group_id" {
|
||||||
|
value = "clc_group.frontends.id"
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Argument Reference
|
||||||
|
|
||||||
|
|
||||||
|
The following arguments are supported:
|
||||||
|
|
||||||
|
* `name` - (Required, string) The name (or GUID) of this server group. Will resolve to existing if present.
|
||||||
|
* `parent` - (Required, string) The name or ID of the parent group. Will error if absent or unable to resolve.
|
||||||
|
* `location_id` - (Required, string) The datacenter location of both parent group and this group.
|
||||||
|
Examples: "WA1", "VA1"
|
||||||
|
* `description` - (Optional, string) Description for server group (visible in control portal only)
|
||||||
|
* `custom_fields` - (Optional) See [CustomFields](#custom_fields) below for details.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<a id="custom_fields"></a>
|
||||||
|
## CustomFields
|
||||||
|
|
||||||
|
`custom_fields` is a block within the configuration that may be
|
||||||
|
repeated to bind custom fields for a server. CustomFields need be set
|
||||||
|
up in advance. Each `custom_fields` block supports the following:
|
||||||
|
|
||||||
|
* `id` - (Required, string) The ID of the custom field to set.
|
||||||
|
* `value` - (Required, string) The value for the specified field.
|
||||||
|
|
|
@ -0,0 +1,45 @@
|
||||||
|
---
|
||||||
|
layout: "clc"
|
||||||
|
page_title: "clc: clc_load_balancer"
|
||||||
|
sidebar_current: "docs-clc-resource-load-balancer"
|
||||||
|
description: |-
|
||||||
|
Manages a CLC load balacner.
|
||||||
|
---
|
||||||
|
|
||||||
|
# clc\_load\_balancer
|
||||||
|
|
||||||
|
Manages a CLC load balancer. Manage connected backends with [clc_load_balancer_pool](load_balancer_pool.html)
|
||||||
|
|
||||||
|
See also [Complete API documentation](https://www.ctl.io/api-docs/v2/#shared-load-balancer).
|
||||||
|
|
||||||
|
## Example Usage
|
||||||
|
|
||||||
|
|
||||||
|
```
|
||||||
|
# Provision a load balancer
|
||||||
|
resource "clc_load_balancer" "api" {
|
||||||
|
data_center = "${clc_group.frontends.location_id}"
|
||||||
|
name = "api"
|
||||||
|
description = "api load balancer"
|
||||||
|
status = "enabled"
|
||||||
|
}
|
||||||
|
|
||||||
|
output "api_ip" {
|
||||||
|
value = "clc_load_balancer.api.ip_address"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Argument Reference
|
||||||
|
|
||||||
|
The following arguments are supported:
|
||||||
|
|
||||||
|
* `name` - (Required, string) The name of the load balancer.
|
||||||
|
* `data_center` - (Required, string) The datacenter location of both parent group and this group.
|
||||||
|
* `status` - (Required, string) Either "enabled" or "disabled"
|
||||||
|
* `description` - (Optional, string) Description for server group (visible in control portal only)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,70 @@
|
||||||
|
---
|
||||||
|
layout: "clc"
|
||||||
|
page_title: "clc: clc_load_balancer_pool"
|
||||||
|
sidebar_current: "docs-clc-resource-load-balancer-pool"
|
||||||
|
description: |-
|
||||||
|
Manages a CLC load balancer pool.
|
||||||
|
---
|
||||||
|
|
||||||
|
# clc\_load\_balancer\_pool
|
||||||
|
|
||||||
|
Manages a CLC load balancer pool. Manage related frontend with [clc_load_balancer](load_balancer.html)
|
||||||
|
|
||||||
|
See also [Complete API documentation](https://www.ctl.io/api-docs/v2/#shared-load-balancer).
|
||||||
|
|
||||||
|
## Example Usage
|
||||||
|
|
||||||
|
|
||||||
|
```
|
||||||
|
# Provision a load balancer pool
|
||||||
|
resource "clc_load_balancer_pool" "pool" {
|
||||||
|
data_center = "${clc_group.frontends.location_id}"
|
||||||
|
load_balancer = "${clc_load_balancer.api.id}"
|
||||||
|
method = "roundRobin"
|
||||||
|
persistence = "standard"
|
||||||
|
port = 80
|
||||||
|
nodes
|
||||||
|
{
|
||||||
|
status = "enabled"
|
||||||
|
ipAddress = "${clc_server.node.0.private_ip_address}"
|
||||||
|
privatePort = 3000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
output "pool" {
|
||||||
|
value = "$join(" ", clc_load_balancer.pool.nodes)}"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Argument Reference
|
||||||
|
|
||||||
|
The following arguments are supported:
|
||||||
|
|
||||||
|
* `load_balancer` - (Required, string) The id of the load balancer.
|
||||||
|
* `data_center` - (Required, string) The datacenter location for this pool.
|
||||||
|
* `port` - (Required, int) Either 80 or 443
|
||||||
|
* `method` - (Optional, string) The configured balancing method. Either
|
||||||
|
"roundRobin" (default) or "leastConnection".
|
||||||
|
* `persistence` - (Optional, string) The configured persistence
|
||||||
|
method. Either "standard" (default) or "sticky".
|
||||||
|
* nodes - (Optional) See [Nodes](#nodes) below for details.
|
||||||
|
|
||||||
|
|
||||||
|
<a id="nodes"></a>
|
||||||
|
## Nodes
|
||||||
|
|
||||||
|
|
||||||
|
`nodes` is a block within the configuration that may be repeated to
|
||||||
|
specify connected nodes on this pool. Each `nodes` block supports the
|
||||||
|
following:
|
||||||
|
|
||||||
|
* `ipAddress` (Required, string) The destination internal ip of pool node.
|
||||||
|
* `privatePort` (Required, int) The destination port on the pool node.
|
||||||
|
* `status` (Optional, string) Either "enabled" or "disabled".
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,85 @@
|
||||||
|
---
|
||||||
|
layout: "clc"
|
||||||
|
page_title: "clc: clc_public_ip"
|
||||||
|
sidebar_current: "docs-clc-resource-public-ip"
|
||||||
|
description: |-
|
||||||
|
Manages a CLC public ip.
|
||||||
|
---
|
||||||
|
|
||||||
|
# clc\_public\_ip
|
||||||
|
|
||||||
|
Manages a CLC public ip (for an existing server).
|
||||||
|
|
||||||
|
See also [Complete API documentation](https://www.ctl.io/api-docs/v2/#public-ip).
|
||||||
|
|
||||||
|
## Example Usage
|
||||||
|
|
||||||
|
```
|
||||||
|
# Provision a public ip
|
||||||
|
resource "clc_public_ip" "backdoor" {
|
||||||
|
server_id = "${clc_server.node.0.id}"
|
||||||
|
internal_ip_address = "${clc_server.node.0.private_ip_address}"
|
||||||
|
ports
|
||||||
|
{
|
||||||
|
protocol = "ICMP"
|
||||||
|
port = -1
|
||||||
|
}
|
||||||
|
ports
|
||||||
|
{
|
||||||
|
protocol = "TCP"
|
||||||
|
port = 22
|
||||||
|
}
|
||||||
|
ports
|
||||||
|
{
|
||||||
|
protocol = "TCP"
|
||||||
|
port = 2000
|
||||||
|
port_to = 9000
|
||||||
|
}
|
||||||
|
source_restrictions
|
||||||
|
{ cidr = "85.39.22.15/30" }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
output "ip" {
|
||||||
|
value = "clc_public_ip.backdoor.id"
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
## Argument Reference
|
||||||
|
|
||||||
|
The following arguments are supported:
|
||||||
|
|
||||||
|
* `server_id` - (Required, string) The name or ID of the server to bind IP to.
|
||||||
|
* `internal_ip_address` - (Required, string) The internal IP of the
|
||||||
|
NIC to attach to. If not provided, a new internal NIC will be
|
||||||
|
provisioned and used.
|
||||||
|
* `ports` - (Optional) See [Ports](#ports) below for details.
|
||||||
|
* `source_restrictions` - (Optional) See
|
||||||
|
[SourceRestrictions](#source_restrictions) below for details.
|
||||||
|
|
||||||
|
|
||||||
|
<a id="ports"></a>
|
||||||
|
## Ports
|
||||||
|
|
||||||
|
`ports` is a block within the configuration that may be
|
||||||
|
repeated to specify open ports on the target IP. Each
|
||||||
|
`ports` block supports the following:
|
||||||
|
|
||||||
|
* `protocol` (Required, string) One of "tcp", "udp", "icmp".
|
||||||
|
* `port` (Required, int) The port to open. If defining a range, demarks starting port
|
||||||
|
* `portTo` (Optional, int) Given a port range, demarks the ending port.
|
||||||
|
|
||||||
|
|
||||||
|
<a id="source_restrictions"></a>
|
||||||
|
## SourceRestrictions
|
||||||
|
|
||||||
|
`source_restrictions` is a block within the configuration that may be
|
||||||
|
repeated to restrict ingress traffic on specified CIDR blocks. Each
|
||||||
|
`source_restrictions` block supports the following:
|
||||||
|
|
||||||
|
* `cidr` (Required, string) The IP or range of IPs in CIDR notation.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,106 @@
|
||||||
|
---
|
||||||
|
layout: "clc"
|
||||||
|
page_title: "clc: clc_server"
|
||||||
|
sidebar_current: "docs-clc-resource-server"
|
||||||
|
description: |-
|
||||||
|
Manages the lifecycle of a CLC server.
|
||||||
|
---
|
||||||
|
|
||||||
|
# clc\_server
|
||||||
|
|
||||||
|
Manages a CLC server.
|
||||||
|
|
||||||
|
See also [Complete API documentation](https://www.ctl.io/api-docs/v2/#servers-create-server).
|
||||||
|
|
||||||
|
## Example Usage
|
||||||
|
|
||||||
|
```
|
||||||
|
# Provision a server
|
||||||
|
resource "clc_server" "node" {
|
||||||
|
name_template = "trusty"
|
||||||
|
source_server_id = "UBUNTU-14-64-TEMPLATE"
|
||||||
|
group_id = "${clc_group.frontends.id}"
|
||||||
|
cpu = 2
|
||||||
|
memory_mb = 2048
|
||||||
|
password = "Green123$"
|
||||||
|
additional_disks
|
||||||
|
{
|
||||||
|
path = "/var"
|
||||||
|
size_gb = 100
|
||||||
|
type = "partitioned"
|
||||||
|
}
|
||||||
|
additional_disks
|
||||||
|
{
|
||||||
|
size_gb = 10
|
||||||
|
type = "raw"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
output "server_id" {
|
||||||
|
value = "clc_server.node.id"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
## Argument Reference
|
||||||
|
|
||||||
|
|
||||||
|
The following arguments are supported:
|
||||||
|
|
||||||
|
* `name_template` - (Required, string) The basename of the server. A unique name will be generated by the platform.
|
||||||
|
* `source_server_id` - (Required, string) The name or ID of the base OS image.
|
||||||
|
Examples: "ubuntu-14-64-template", "rhel-7-64-template", "win2012r2dtc-64"
|
||||||
|
* `group_id` - (Required, string) The name or ID of the server group to spawn server into.
|
||||||
|
* `cpu` - (Required, int) The number of virtual cores
|
||||||
|
* `memory_mb` - (Required, int) Provisioned RAM
|
||||||
|
* `type` - (Required, string) The virtualization type
|
||||||
|
One of "standard", "hyperscale", "bareMetal"
|
||||||
|
* `password` - (Optional, string) The root/adminstrator password. Will be generated by platform if not provided.
|
||||||
|
* `description` - (Optional, string) Description for server (visible in control portal only)
|
||||||
|
* `power_state` - (Optional, string) See [PowerStates](#power_states) below for details.
|
||||||
|
If absent, defaults to `started`.
|
||||||
|
* `private_ip_address` - (Optional, string) Set internal IP address. If absent, allocated and assigned from pool.
|
||||||
|
* `network_id` - (Optional, string) GUID of network to use. (Must be set up in advance from control portal.)
|
||||||
|
When absent, the default network will be used.
|
||||||
|
* `storage_type` - (Optional, string) Backup and replication strategy for disks.
|
||||||
|
One of "standard", "premium"
|
||||||
|
* `additional_disks` - (Optional) See [Disks](#disks) below for details.
|
||||||
|
* `custom_fields` - (Optional) See [CustomFields](#custom_fields) below for details.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<a id="power_states"></a>
|
||||||
|
## PowerStates
|
||||||
|
|
||||||
|
`power_state` may be used to set initial power state or modify existing instances.
|
||||||
|
|
||||||
|
* `on` | `started` - machine powered on
|
||||||
|
* `off` | `stopped` - machine powered off forcefully
|
||||||
|
* `paused` - freeze machine: memory, processes, billing, monitoring.
|
||||||
|
* `shutdown` - shutdown gracefully
|
||||||
|
* `reboot` - restart gracefully
|
||||||
|
* `reset` - restart forcefully
|
||||||
|
|
||||||
|
<a id="disks"></a>
|
||||||
|
## Disks
|
||||||
|
|
||||||
|
`additional_disks` is a block within the configuration that may be
|
||||||
|
repeated to specify the attached disks on a server. Each
|
||||||
|
`additional_disks` block supports the following:
|
||||||
|
|
||||||
|
* `type` - (Required, string) Either "raw" or "partitioned".
|
||||||
|
* `size_gb` - (Required, int) Size of allocated disk.
|
||||||
|
* `path` - (Required, string, type:`partitioned`) The mountpoint for the disk.
|
||||||
|
|
||||||
|
|
||||||
|
<a id="custom_fields"></a>
|
||||||
|
## CustomFields
|
||||||
|
|
||||||
|
`custom_fields` is a block within the configuration that may be
|
||||||
|
repeated to bind custom fields for a server. CustomFields need be set
|
||||||
|
up in advance. Each `custom_fields` block supports the following:
|
||||||
|
|
||||||
|
* `id` - (Required, string) The ID of the custom field to set.
|
||||||
|
* `value` - (Required, string) The value for the specified field.
|
||||||
|
|
|
@ -0,0 +1,38 @@
|
||||||
|
<% 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/providers/index.html">« Documentation Home</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li<%= sidebar_current("docs-clc-index") %>>
|
||||||
|
<a href="/docs/providers/clc/index.html">CenturyLinkCloud Provider</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li<%= sidebar_current(/^docs-clc-resource/) %>>
|
||||||
|
<a href="#">Resources</a>
|
||||||
|
<ul class="nav nav-visible">
|
||||||
|
<li<%= sidebar_current("docs-clc-resource-server") %>>
|
||||||
|
<a href="/docs/providers/clc/r/server.html">clc_server</a>
|
||||||
|
</li>
|
||||||
|
<li<%= sidebar_current("docs-clc-resource-group") %>>
|
||||||
|
<a href="/docs/providers/clc/r/group.html">clc_group</a>
|
||||||
|
</li>
|
||||||
|
<li<%= sidebar_current("docs-clc-resource-public-ip") %>>
|
||||||
|
<a href="/docs/providers/clc/r/public_ip.html">clc_public_ip</a>
|
||||||
|
</li>
|
||||||
|
<li<%= sidebar_current("docs-clc-resource-load-balancer") %>>
|
||||||
|
<a href="/docs/providers/clc/r/load_balancer.html">clc_load_balancer</a>
|
||||||
|
</li>
|
||||||
|
<li<%= sidebar_current("docs-clc-resource-load-balancer-pool") %>>
|
||||||
|
<a href="/docs/providers/clc/r/load_balancer_pool.html">clc_load_balancer_pool</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<%= yield %>
|
||||||
|
<% end %>
|
|
@ -149,6 +149,10 @@
|
||||||
<a href="/docs/providers/chef/index.html">Chef</a>
|
<a href="/docs/providers/chef/index.html">Chef</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
|
<li<%= sidebar_current("docs-providers-clc") %>>
|
||||||
|
<a href="/docs/providers/clc/index.html">CenturyLinkCloud</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>
|
||||||
|
|
Loading…
Reference in New Issue