Adding support for replicas
diff --git a/rackspace/db/v1/instances/delegate.go b/rackspace/db/v1/instances/delegate.go
index d2e3c25..ca223eb 100644
--- a/rackspace/db/v1/instances/delegate.go
+++ b/rackspace/db/v1/instances/delegate.go
@@ -58,6 +58,8 @@
// - You can create new users or databases if you want, but they cannot be
// the same as the ones from the instance that was backed up.
RestorePoint string
+
+ ReplicaOf string
}
func (opts CreateOpts) ToInstanceCreateMap() (map[string]interface{}, error) {
@@ -91,6 +93,10 @@
instance["restorePoint"] = map[string]string{"backupRef": opts.RestorePoint}
}
+ if opts.ReplicaOf != "" {
+ instance["replica_of"] = opts.ReplicaOf
+ }
+
return map[string]interface{}{"instance": instance}, nil
}
diff --git a/rackspace/db/v1/instances/fixtures.go b/rackspace/db/v1/instances/fixtures.go
index 55dc0a8..e144099 100644
--- a/rackspace/db/v1/instances/fixtures.go
+++ b/rackspace/db/v1/instances/fixtures.go
@@ -96,6 +96,104 @@
})
}
+func HandleCreateReplicaSuccessfully(t *testing.T) {
+ th.Mux.HandleFunc("/instances", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "POST")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+ th.TestJSONRequest(t, r, `
+{
+ "instance": {
+ "volume": {
+ "size": 1
+ },
+ "flavorRef": "9",
+ "name": "t2s1_ALT_GUEST",
+ "replica_of": "6bdca2fc-418e-40bd-a595-62abda61862d"
+ }
+}
+`)
+
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+
+ fmt.Fprintf(w, `
+{
+ "instance": {
+ "status": "BUILD",
+ "updated": "2014-10-14T18:42:15",
+ "name": "t2s1_ALT_GUEST",
+ "links": [
+ {
+ "href": "https://ord.databases.api.rackspacecloud.com/v1.0/5919009/instances/8367c312-7c40-4a66-aab1-5767478914fc",
+ "rel": "self"
+ },
+ {
+ "href": "https://ord.databases.api.rackspacecloud.com/instances/8367c312-7c40-4a66-aab1-5767478914fc",
+ "rel": "bookmark"
+ }
+ ],
+ "created": "2014-10-14T18:42:15",
+ "id": "8367c312-7c40-4a66-aab1-5767478914fc",
+ "volume": {"size": 1},
+ "flavor": {"id": "9"},
+ "datastore": {
+ "version": "5.6",
+ "type": "mysql"
+ },
+ "replica_of": {"id": "6bdca2fc-418e-40bd-a595-62abda61862d"}
+ }
+}
+`)
+ })
+}
+
+func HandleListReplicasSuccessfully(t *testing.T) {
+ th.Mux.HandleFunc("/instances", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "GET")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+ w.Header().Add("Content-Type", "application/json")
+
+ fmt.Fprintf(w, `
+{
+ "instances": [
+ {
+ "status": "ACTIVE",
+ "name": "t1s1_ALT_GUEST",
+ "links": [
+ {
+ "href": "https://ord.databases.api.rackspacecloud.com/v1.0/1234/instances/3c691f06-bf9a-4618-b7ec-2817ce0cf254",
+ "rel": "self"
+ },
+ {
+ "href": "https://ord.databases.api.rackspacecloud.com/instances/3c691f06-bf9a-4618-b7ec-2817ce0cf254",
+ "rel": "bookmark"
+ }
+ ],
+ "ip": [
+ "10.0.0.3"
+ ],
+ "id": "3c691f06-bf9a-4618-b7ec-2817ce0cf254",
+ "volume": {
+ "size": 1
+ },
+ "flavor": {
+ "id": "9"
+ },
+ "datastore": {
+ "version": "5.6",
+ "type": "mysql"
+ },
+ "replica_of": {
+ "id": "8b499b45-52d6-402d-b398-f9d8f279c69a"
+ }
+ }
+ ]
+}
+`)
+ })
+}
+
func HandleGetInstanceSuccessfully(t *testing.T, id string) {
th.Mux.HandleFunc("/instances/"+id, func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "GET")
@@ -107,6 +205,42 @@
})
}
+func HandleGetReplicaSuccessfully(t *testing.T, id string) {
+ th.Mux.HandleFunc("/instances/"+id, func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "GET")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+ w.Header().Add("Content-Type", "application/json")
+
+ fmt.Fprintf(w, `
+{
+ "instance": {
+ "status": "ACTIVE",
+ "updated": "2014-09-26T19:15:57",
+ "name": "t1_ALT_GUEST",
+ "created": "2014-09-26T19:15:50",
+ "ip": [
+ "10.0.0.2"
+ ],
+ "replicas": [
+ {"id": "3c691f06-bf9a-4618-b7ec-2817ce0cf254"}
+ ],
+ "id": "8b499b45-52d6-402d-b398-f9d8f279c69a",
+ "volume": {
+ "used": 0.54,
+ "size": 1
+ },
+ "flavor": {"id": "9"},
+ "datastore": {
+ "version": "5.6",
+ "type": "mysql"
+ }
+ }
+}
+`)
+ })
+}
+
func HandleGetConfigSuccessfully(t *testing.T, id string) {
th.Mux.HandleFunc("/instances/"+id+"/configuration", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "GET")
@@ -212,3 +346,22 @@
`)
})
}
+
+func HandleDetachReplicaSuccessfully(t *testing.T, id string) {
+ th.Mux.HandleFunc("/instances/"+id, func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "PATCH")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+ th.TestJSONRequest(t, r, `
+{
+ "instance": {
+ "replica_of": "",
+ "slave_of": ""
+ }
+}
+`)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusAccepted)
+ })
+}
diff --git a/rackspace/db/v1/instances/requests.go b/rackspace/db/v1/instances/requests.go
index b311289..66bc872 100644
--- a/rackspace/db/v1/instances/requests.go
+++ b/rackspace/db/v1/instances/requests.go
@@ -41,3 +41,15 @@
}
return pagination.NewPager(client, backupsURL(client, instanceID), pageFn)
}
+
+func DetachReplica(client *gophercloud.ServiceClient, replicaID string) DetachResult {
+ var res DetachResult
+
+ _, res.Err = perigee.Request("PATCH", resourceURL(client, replicaID), perigee.Options{
+ MoreHeaders: client.AuthenticatedHeaders(),
+ ReqBody: map[string]interface{}{"instance": map[string]string{"replica_of": "", "slave_of": ""}},
+ OkCodes: []int{202},
+ })
+
+ return res
+}
diff --git a/rackspace/db/v1/instances/requests_test.go b/rackspace/db/v1/instances/requests_test.go
index 66d58fd..2cafa22 100644
--- a/rackspace/db/v1/instances/requests_test.go
+++ b/rackspace/db/v1/instances/requests_test.go
@@ -3,12 +3,32 @@
import (
"testing"
+ "github.com/rackspace/gophercloud"
+ os "github.com/rackspace/gophercloud/openstack/db/v1/instances"
"github.com/rackspace/gophercloud/pagination"
"github.com/rackspace/gophercloud/rackspace/db/v1/backups"
th "github.com/rackspace/gophercloud/testhelper"
fake "github.com/rackspace/gophercloud/testhelper/client"
)
+var expectedReplica = &Instance{
+ Status: "BUILD",
+ Updated: "2014-10-14T18:42:15",
+ Name: "t2s1_ALT_GUEST",
+ Links: []gophercloud.Link{
+ gophercloud.Link{Rel: "self", Href: "https://ord.databases.api.rackspacecloud.com/v1.0/5919009/instances/8367c312-7c40-4a66-aab1-5767478914fc"},
+ gophercloud.Link{Rel: "bookmark", Href: "https://ord.databases.api.rackspacecloud.com/instances/8367c312-7c40-4a66-aab1-5767478914fc"},
+ },
+ Created: "2014-10-14T18:42:15",
+ ID: "8367c312-7c40-4a66-aab1-5767478914fc",
+ Volume: os.Volume{Size: 1},
+ Flavor: os.Flavor{ID: "9"},
+ Datastore: Datastore{Version: "5.6", Type: "mysql"},
+ ReplicaOf: &Instance{
+ ID: "6bdca2fc-418e-40bd-a595-62abda61862d",
+ },
+}
+
func TestGetConfig(t *testing.T) {
th.SetupHTTP()
defer th.TeardownHTTP()
@@ -114,3 +134,113 @@
t.Errorf("Expected 1 page, got %d", count)
}
}
+
+func TestCreateReplica(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ HandleCreateReplicaSuccessfully(t)
+
+ opts := CreateOpts{
+ Name: "t2s1_ALT_GUEST",
+ FlavorRef: "9",
+ Size: 1,
+ ReplicaOf: "6bdca2fc-418e-40bd-a595-62abda61862d",
+ }
+
+ replica, err := Create(fake.ServiceClient(), opts).Extract()
+ th.AssertNoErr(t, err)
+
+ th.AssertDeepEquals(t, expectedReplica, replica)
+}
+
+func TestListReplicas(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ HandleListReplicasSuccessfully(t)
+
+ pages := 0
+ err := List(fake.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) {
+ pages++
+
+ actual, err := ExtractInstances(page)
+ if err != nil {
+ return false, err
+ }
+
+ expected := []Instance{
+ Instance{
+ Status: "ACTIVE",
+ Name: "t1s1_ALT_GUEST",
+ Links: []gophercloud.Link{
+ gophercloud.Link{Rel: "self", Href: "https://ord.databases.api.rackspacecloud.com/v1.0/1234/instances/3c691f06-bf9a-4618-b7ec-2817ce0cf254"},
+ gophercloud.Link{Rel: "bookmark", Href: "https://ord.databases.api.rackspacecloud.com/instances/3c691f06-bf9a-4618-b7ec-2817ce0cf254"},
+ },
+ ID: "3c691f06-bf9a-4618-b7ec-2817ce0cf254",
+ IP: []string{"10.0.0.3"},
+ Volume: os.Volume{Size: 1},
+ Flavor: os.Flavor{ID: "9"},
+ Datastore: Datastore{Version: "5.6", Type: "mysql"},
+ ReplicaOf: &Instance{
+ ID: "8b499b45-52d6-402d-b398-f9d8f279c69a",
+ },
+ },
+ }
+
+ th.CheckDeepEquals(t, expected, actual)
+
+ return true, nil
+ })
+
+ th.AssertNoErr(t, err)
+
+ if pages != 1 {
+ t.Errorf("Expected 1 page, saw %d", pages)
+ }
+}
+
+func TestGetReplica(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ HandleGetReplicaSuccessfully(t, instanceID)
+
+ replica, err := Get(fake.ServiceClient(), instanceID).Extract()
+ th.AssertNoErr(t, err)
+
+ expectedReplica := &Instance{
+ Status: "ACTIVE",
+ Updated: "2014-09-26T19:15:57",
+ Name: "t1_ALT_GUEST",
+ Created: "2014-09-26T19:15:50",
+ IP: []string{
+ "10.0.0.2",
+ },
+ Replicas: []Instance{
+ Instance{ID: "3c691f06-bf9a-4618-b7ec-2817ce0cf254"},
+ },
+ ID: "8b499b45-52d6-402d-b398-f9d8f279c69a",
+ Volume: os.Volume{
+ Used: 0.54,
+ Size: 1,
+ },
+ Flavor: os.Flavor{ID: "9"},
+ Datastore: Datastore{
+ Version: "5.6",
+ Type: "mysql",
+ },
+ }
+
+ th.AssertDeepEquals(t, replica, expectedReplica)
+}
+
+func TestDetachReplica(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ HandleDetachReplicaSuccessfully(t, "{replicaID}")
+
+ err := DetachReplica(fake.ServiceClient(), "{replicaID}").ExtractErr()
+ th.AssertNoErr(t, err)
+}
diff --git a/rackspace/db/v1/instances/results.go b/rackspace/db/v1/instances/results.go
index d2eff6b..760e5a7 100644
--- a/rackspace/db/v1/instances/results.go
+++ b/rackspace/db/v1/instances/results.go
@@ -4,6 +4,7 @@
"github.com/mitchellh/mapstructure"
"github.com/rackspace/gophercloud"
os "github.com/rackspace/gophercloud/openstack/db/v1/instances"
+ "github.com/rackspace/gophercloud/pagination"
)
type Datastore struct {
@@ -47,6 +48,12 @@
// Information about the attached volume of the instance.
Volume os.Volume
+
+ IP []string
+
+ ReplicaOf *Instance `mapstructure:"replica_of" json:"replica_of"`
+
+ Replicas []Instance
}
func commonExtract(err error, body interface{}) (*Instance, error) {
@@ -85,6 +92,10 @@
gophercloud.Result
}
+type DetachResult struct {
+ gophercloud.ErrResult
+}
+
// Extract will extract the configuration information (in the form of a map)
// about a particular instance.
func (r ConfigResult) Extract() (map[string]string, error) {
@@ -106,3 +117,14 @@
type UpdateResult struct {
gophercloud.ErrResult
}
+
+func ExtractInstances(page pagination.Page) ([]Instance, error) {
+ casted := page.(os.InstancePage).Body
+
+ var response struct {
+ Instances []Instance `mapstructure:"instances"`
+ }
+
+ err := mapstructure.Decode(casted, &response)
+ return response.Instances, err
+}