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",
|
||||
"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",
|
||||
"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",
|
||||
"Rev": "8cbe86575f00210a6df2c19cb2f59b00cd181de3"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/CenturyLinkCloud/clc-sdk",
|
||||
"Rev": "74abd779894192c559ad29f0183a1716370490ad"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/apparentlymart/go-cidr/cidr",
|
||||
"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-chef,
|
||||
body.layout-azurerm,
|
||||
body.layout-clc,
|
||||
body.layout-cloudflare,
|
||||
body.layout-cloudstack,
|
||||
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>
|
||||
</li>
|
||||
|
||||
<li<%= sidebar_current("docs-providers-clc") %>>
|
||||
<a href="/docs/providers/clc/index.html">CenturyLinkCloud</a>
|
||||
</li>
|
||||
|
||||
<li<%= sidebar_current("docs-providers-cloudflare") %>>
|
||||
<a href="/docs/providers/cloudflare/index.html">CloudFlare</a>
|
||||
</li>
|
||||
|
|
Loading…
Reference in New Issue