Merge pull request #494 from timbyr/RouterExtraRoutes
[rfr]Support Extra routes extension in router api
diff --git a/acceptance/openstack/db/v1/common.go b/acceptance/openstack/db/v1/common.go
new file mode 100644
index 0000000..f7ffc37
--- /dev/null
+++ b/acceptance/openstack/db/v1/common.go
@@ -0,0 +1,70 @@
+// +build acceptance db
+
+package v1
+
+import (
+ "os"
+ "testing"
+
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/openstack"
+ "github.com/rackspace/gophercloud/openstack/db/v1/instances"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func newClient(t *testing.T) *gophercloud.ServiceClient {
+ ao, err := openstack.AuthOptionsFromEnv()
+ th.AssertNoErr(t, err)
+
+ client, err := openstack.AuthenticatedClient(ao)
+ th.AssertNoErr(t, err)
+
+ c, err := openstack.NewDBV1(client, gophercloud.EndpointOpts{
+ Region: os.Getenv("OS_REGION_NAME"),
+ })
+ th.AssertNoErr(t, err)
+
+ return c
+}
+
+type context struct {
+ test *testing.T
+ client *gophercloud.ServiceClient
+ instanceID string
+ DBIDs []string
+ users []string
+}
+
+func newContext(t *testing.T) context {
+ return context{
+ test: t,
+ client: newClient(t),
+ }
+}
+
+func (c context) Logf(msg string, args ...interface{}) {
+ if len(args) > 0 {
+ c.test.Logf(msg, args...)
+ } else {
+ c.test.Log(msg)
+ }
+}
+
+func (c context) AssertNoErr(err error) {
+ th.AssertNoErr(c.test, err)
+}
+
+func (c context) WaitUntilActive(id string) {
+ err := gophercloud.WaitFor(60, func() (bool, error) {
+ inst, err := instances.Get(c.client, id).Extract()
+ if err != nil {
+ return false, err
+ }
+ if inst.Status == "ACTIVE" {
+ return true, nil
+ }
+ return false, nil
+ })
+
+ c.AssertNoErr(err)
+}
diff --git a/acceptance/openstack/db/v1/database_test.go b/acceptance/openstack/db/v1/database_test.go
new file mode 100644
index 0000000..2fd3175
--- /dev/null
+++ b/acceptance/openstack/db/v1/database_test.go
@@ -0,0 +1,45 @@
+// +build acceptance db
+
+package v1
+
+import (
+ db "github.com/rackspace/gophercloud/openstack/db/v1/databases"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+func (c context) createDBs() {
+ opts := db.BatchCreateOpts{
+ db.CreateOpts{Name: "db1"},
+ db.CreateOpts{Name: "db2"},
+ db.CreateOpts{Name: "db3"},
+ }
+
+ err := db.Create(c.client, c.instanceID, opts).ExtractErr()
+ c.AssertNoErr(err)
+ c.Logf("Created three databases on instance %s: db1, db2, db3", c.instanceID)
+}
+
+func (c context) listDBs() {
+ c.Logf("Listing databases on instance %s", c.instanceID)
+
+ err := db.List(c.client, c.instanceID).EachPage(func(page pagination.Page) (bool, error) {
+ dbList, err := db.ExtractDBs(page)
+ c.AssertNoErr(err)
+
+ for _, db := range dbList {
+ c.Logf("DB: %#v", db)
+ }
+
+ return true, nil
+ })
+
+ c.AssertNoErr(err)
+}
+
+func (c context) deleteDBs() {
+ for _, id := range []string{"db1", "db2", "db3"} {
+ err := db.Delete(c.client, c.instanceID, id).ExtractErr()
+ c.AssertNoErr(err)
+ c.Logf("Deleted DB %s", id)
+ }
+}
diff --git a/acceptance/openstack/db/v1/flavor_test.go b/acceptance/openstack/db/v1/flavor_test.go
new file mode 100644
index 0000000..46f986c
--- /dev/null
+++ b/acceptance/openstack/db/v1/flavor_test.go
@@ -0,0 +1,31 @@
+// +build acceptance db
+
+package v1
+
+import (
+ "github.com/rackspace/gophercloud/openstack/db/v1/flavors"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+func (c context) listFlavors() {
+ c.Logf("Listing flavors")
+
+ err := flavors.List(c.client).EachPage(func(page pagination.Page) (bool, error) {
+ flavorList, err := flavors.ExtractFlavors(page)
+ c.AssertNoErr(err)
+
+ for _, f := range flavorList {
+ c.Logf("Flavor: ID [%s] Name [%s] RAM [%d]", f.ID, f.Name, f.RAM)
+ }
+
+ return true, nil
+ })
+
+ c.AssertNoErr(err)
+}
+
+func (c context) getFlavor() {
+ flavor, err := flavors.Get(c.client, "1").Extract()
+ c.Logf("Getting flavor %s", flavor.ID)
+ c.AssertNoErr(err)
+}
diff --git a/acceptance/openstack/db/v1/instance_test.go b/acceptance/openstack/db/v1/instance_test.go
new file mode 100644
index 0000000..dfded21
--- /dev/null
+++ b/acceptance/openstack/db/v1/instance_test.go
@@ -0,0 +1,138 @@
+// +build acceptance db
+
+package v1
+
+import (
+ "os"
+ "testing"
+
+ "github.com/rackspace/gophercloud/acceptance/tools"
+ "github.com/rackspace/gophercloud/openstack/db/v1/instances"
+ "github.com/rackspace/gophercloud/pagination"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+const envDSType = "DATASTORE_TYPE_ID"
+
+func TestRunner(t *testing.T) {
+ c := newContext(t)
+
+ // FLAVOR tests
+ c.listFlavors()
+ c.getFlavor()
+
+ // INSTANCE tests
+ c.createInstance()
+ c.listInstances()
+ c.getInstance()
+ c.isRootEnabled()
+ c.enableRootUser()
+ c.isRootEnabled()
+ c.restartInstance()
+ //c.resizeInstance()
+ //c.resizeVol()
+
+ // DATABASE tests
+ c.createDBs()
+ c.listDBs()
+
+ // USER tests
+ c.createUsers()
+ c.listUsers()
+
+ // TEARDOWN
+ c.deleteUsers()
+ c.deleteDBs()
+ c.deleteInstance()
+}
+
+func (c context) createInstance() {
+ if os.Getenv(envDSType) == "" {
+ c.test.Fatalf("%s must be set as an environment var", envDSType)
+ }
+
+ opts := instances.CreateOpts{
+ FlavorRef: "2",
+ Size: 5,
+ Name: tools.RandomString("gopher_db", 5),
+ Datastore: &instances.DatastoreOpts{Type: os.Getenv(envDSType)},
+ }
+
+ instance, err := instances.Create(c.client, opts).Extract()
+ th.AssertNoErr(c.test, err)
+
+ c.Logf("Restarting %s. Waiting...", instance.ID)
+ c.WaitUntilActive(instance.ID)
+ c.Logf("Created Instance %s", instance.ID)
+
+ c.instanceID = instance.ID
+}
+
+func (c context) listInstances() {
+ c.Logf("Listing instances")
+
+ err := instances.List(c.client).EachPage(func(page pagination.Page) (bool, error) {
+ instanceList, err := instances.ExtractInstances(page)
+ c.AssertNoErr(err)
+
+ for _, i := range instanceList {
+ c.Logf("Instance: ID [%s] Name [%s] Status [%s] VolSize [%d] Datastore Type [%s]",
+ i.ID, i.Name, i.Status, i.Volume.Size, i.Datastore.Type)
+ }
+
+ return true, nil
+ })
+
+ c.AssertNoErr(err)
+}
+
+func (c context) getInstance() {
+ instance, err := instances.Get(c.client, c.instanceID).Extract()
+ c.AssertNoErr(err)
+ c.Logf("Getting instance: %s", instance.ID)
+}
+
+func (c context) deleteInstance() {
+ err := instances.Delete(c.client, c.instanceID).ExtractErr()
+ c.AssertNoErr(err)
+ c.Logf("Deleted instance %s", c.instanceID)
+}
+
+func (c context) enableRootUser() {
+ _, err := instances.EnableRootUser(c.client, c.instanceID).Extract()
+ c.AssertNoErr(err)
+ c.Logf("Enabled root user on %s", c.instanceID)
+}
+
+func (c context) isRootEnabled() {
+ enabled, err := instances.IsRootEnabled(c.client, c.instanceID)
+ c.AssertNoErr(err)
+ c.Logf("Is root enabled? %d", enabled)
+}
+
+func (c context) restartInstance() {
+ id := c.instanceID
+ err := instances.Restart(c.client, id).ExtractErr()
+ c.AssertNoErr(err)
+ c.Logf("Restarting %s. Waiting...", id)
+ c.WaitUntilActive(id)
+ c.Logf("Restarted %s", id)
+}
+
+func (c context) resizeInstance() {
+ id := c.instanceID
+ err := instances.Resize(c.client, id, "3").ExtractErr()
+ c.AssertNoErr(err)
+ c.Logf("Resizing %s. Waiting...", id)
+ c.WaitUntilActive(id)
+ c.Logf("Resized %s with flavorRef %s", id, "2")
+}
+
+func (c context) resizeVol() {
+ id := c.instanceID
+ err := instances.ResizeVolume(c.client, id, 4).ExtractErr()
+ c.AssertNoErr(err)
+ c.Logf("Resizing volume of %s. Waiting...", id)
+ c.WaitUntilActive(id)
+ c.Logf("Resized the volume of %s to %d GB", id, 2)
+}
diff --git a/acceptance/openstack/db/v1/pkg.go b/acceptance/openstack/db/v1/pkg.go
new file mode 100644
index 0000000..b7b1f99
--- /dev/null
+++ b/acceptance/openstack/db/v1/pkg.go
@@ -0,0 +1 @@
+package v1
diff --git a/acceptance/openstack/db/v1/user_test.go b/acceptance/openstack/db/v1/user_test.go
new file mode 100644
index 0000000..25a4794
--- /dev/null
+++ b/acceptance/openstack/db/v1/user_test.go
@@ -0,0 +1,70 @@
+// +build acceptance db
+
+package v1
+
+import (
+ "github.com/rackspace/gophercloud/acceptance/tools"
+ db "github.com/rackspace/gophercloud/openstack/db/v1/databases"
+ u "github.com/rackspace/gophercloud/openstack/db/v1/users"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+func (c context) createUsers() {
+ users := []string{
+ tools.RandomString("user_", 5),
+ tools.RandomString("user_", 5),
+ tools.RandomString("user_", 5),
+ }
+
+ db1 := db.CreateOpts{Name: "db1"}
+ db2 := db.CreateOpts{Name: "db2"}
+ db3 := db.CreateOpts{Name: "db3"}
+
+ opts := u.BatchCreateOpts{
+ u.CreateOpts{
+ Name: users[0],
+ Password: tools.RandomString("", 5),
+ Databases: db.BatchCreateOpts{db1, db2, db3},
+ },
+ u.CreateOpts{
+ Name: users[1],
+ Password: tools.RandomString("", 5),
+ Databases: db.BatchCreateOpts{db1, db2},
+ },
+ u.CreateOpts{
+ Name: users[2],
+ Password: tools.RandomString("", 5),
+ Databases: db.BatchCreateOpts{db3},
+ },
+ }
+
+ err := u.Create(c.client, c.instanceID, opts).ExtractErr()
+ c.AssertNoErr(err)
+ c.Logf("Created three users on instance %s: %s, %s, %s", c.instanceID, users[0], users[1], users[2])
+ c.users = users
+}
+
+func (c context) listUsers() {
+ c.Logf("Listing databases on instance %s", c.instanceID)
+
+ err := db.List(c.client, c.instanceID).EachPage(func(page pagination.Page) (bool, error) {
+ dbList, err := db.ExtractDBs(page)
+ c.AssertNoErr(err)
+
+ for _, db := range dbList {
+ c.Logf("DB: %#v", db)
+ }
+
+ return true, nil
+ })
+
+ c.AssertNoErr(err)
+}
+
+func (c context) deleteUsers() {
+ for _, id := range c.DBIDs {
+ err := db.Delete(c.client, c.instanceID, id).ExtractErr()
+ c.AssertNoErr(err)
+ c.Logf("Deleted DB %s", id)
+ }
+}
diff --git a/acceptance/rackspace/db/v1/backup_test.go b/acceptance/rackspace/db/v1/backup_test.go
new file mode 100644
index 0000000..522aace
--- /dev/null
+++ b/acceptance/rackspace/db/v1/backup_test.go
@@ -0,0 +1,84 @@
+// +build acceptance db rackspace
+
+package v1
+
+import (
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/acceptance/tools"
+ "github.com/rackspace/gophercloud/pagination"
+
+ "github.com/rackspace/gophercloud/rackspace/db/v1/backups"
+ "github.com/rackspace/gophercloud/rackspace/db/v1/instances"
+)
+
+func (c *context) createBackup() {
+ opts := backups.CreateOpts{
+ Name: tools.RandomString("backup_", 5),
+ InstanceID: c.instanceID,
+ }
+
+ backup, err := backups.Create(c.client, opts).Extract()
+
+ c.Logf("Created backup %#v", backup)
+ c.AssertNoErr(err)
+
+ err = gophercloud.WaitFor(60, func() (bool, error) {
+ b, err := backups.Get(c.client, backup.ID).Extract()
+ if err != nil {
+ return false, err
+ }
+ if b.Status == "COMPLETED" {
+ return true, nil
+ }
+ return false, nil
+ })
+ c.AssertNoErr(err)
+
+ c.backupID = backup.ID
+}
+
+func (c *context) getBackup() {
+ backup, err := backups.Get(c.client, c.backupID).Extract()
+ c.AssertNoErr(err)
+ c.Logf("Getting backup %s", backup.ID)
+}
+
+func (c *context) listAllBackups() {
+ c.Logf("Listing backups")
+
+ err := backups.List(c.client, nil).EachPage(func(page pagination.Page) (bool, error) {
+ backupList, err := backups.ExtractBackups(page)
+ c.AssertNoErr(err)
+
+ for _, b := range backupList {
+ c.Logf("Backup: %#v", b)
+ }
+
+ return true, nil
+ })
+
+ c.AssertNoErr(err)
+}
+
+func (c *context) listInstanceBackups() {
+ c.Logf("Listing backups for instance %s", c.instanceID)
+
+ err := instances.ListBackups(c.client, c.instanceID).EachPage(func(page pagination.Page) (bool, error) {
+ backupList, err := backups.ExtractBackups(page)
+ c.AssertNoErr(err)
+
+ for _, b := range backupList {
+ c.Logf("Backup: %#v", b)
+ }
+
+ return true, nil
+ })
+
+ c.AssertNoErr(err)
+}
+
+func (c *context) deleteBackup() {
+ err := backups.Delete(c.client, c.backupID).ExtractErr()
+ c.AssertNoErr(err)
+ c.Logf("Deleted backup %s", c.backupID)
+}
diff --git a/acceptance/rackspace/db/v1/common.go b/acceptance/rackspace/db/v1/common.go
new file mode 100644
index 0000000..24512b9
--- /dev/null
+++ b/acceptance/rackspace/db/v1/common.go
@@ -0,0 +1,73 @@
+// +build acceptance db rackspace
+
+package v1
+
+import (
+ "testing"
+
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/acceptance/tools"
+ "github.com/rackspace/gophercloud/rackspace"
+ "github.com/rackspace/gophercloud/rackspace/db/v1/instances"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func newClient(t *testing.T) *gophercloud.ServiceClient {
+ opts, err := rackspace.AuthOptionsFromEnv()
+ th.AssertNoErr(t, err)
+ opts = tools.OnlyRS(opts)
+
+ client, err := rackspace.AuthenticatedClient(opts)
+ th.AssertNoErr(t, err)
+
+ c, err := rackspace.NewDBV1(client, gophercloud.EndpointOpts{
+ Region: "IAD",
+ })
+ th.AssertNoErr(t, err)
+
+ return c
+}
+
+type context struct {
+ test *testing.T
+ client *gophercloud.ServiceClient
+ instanceID string
+ DBIDs []string
+ replicaID string
+ backupID string
+ configGroupID string
+ users []string
+}
+
+func newContext(t *testing.T) context {
+ return context{
+ test: t,
+ client: newClient(t),
+ }
+}
+
+func (c context) Logf(msg string, args ...interface{}) {
+ if len(args) > 0 {
+ c.test.Logf(msg, args...)
+ } else {
+ c.test.Log(msg)
+ }
+}
+
+func (c context) AssertNoErr(err error) {
+ th.AssertNoErr(c.test, err)
+}
+
+func (c context) WaitUntilActive(id string) {
+ err := gophercloud.WaitFor(60, func() (bool, error) {
+ inst, err := instances.Get(c.client, id).Extract()
+ if err != nil {
+ return false, err
+ }
+ if inst.Status == "ACTIVE" {
+ return true, nil
+ }
+ return false, nil
+ })
+ c.AssertNoErr(err)
+}
diff --git a/acceptance/rackspace/db/v1/config_group_test.go b/acceptance/rackspace/db/v1/config_group_test.go
new file mode 100644
index 0000000..81bd40a
--- /dev/null
+++ b/acceptance/rackspace/db/v1/config_group_test.go
@@ -0,0 +1,93 @@
+// +build acceptance db rackspace
+
+package v1
+
+import (
+ "github.com/rackspace/gophercloud/acceptance/tools"
+ os "github.com/rackspace/gophercloud/openstack/db/v1/configurations"
+ "github.com/rackspace/gophercloud/pagination"
+ config "github.com/rackspace/gophercloud/rackspace/db/v1/configurations"
+ "github.com/rackspace/gophercloud/rackspace/db/v1/instances"
+)
+
+func (c *context) createConfigGrp() {
+ opts := os.CreateOpts{
+ Name: tools.RandomString("config_", 5),
+ Values: map[string]interface{}{
+ "connect_timeout": 300,
+ "join_buffer_size": 900000,
+ },
+ }
+
+ cg, err := config.Create(c.client, opts).Extract()
+
+ c.AssertNoErr(err)
+ c.Logf("Created config group %#v", cg)
+
+ c.configGroupID = cg.ID
+}
+
+func (c *context) getConfigGrp() {
+ cg, err := config.Get(c.client, c.configGroupID).Extract()
+ c.Logf("Getting config group: %#v", cg)
+ c.AssertNoErr(err)
+}
+
+func (c *context) updateConfigGrp() {
+ opts := os.UpdateOpts{
+ Name: tools.RandomString("new_name_", 5),
+ Values: map[string]interface{}{
+ "connect_timeout": 250,
+ },
+ }
+ err := config.Update(c.client, c.configGroupID, opts).ExtractErr()
+ c.Logf("Updated config group %s", c.configGroupID)
+ c.AssertNoErr(err)
+}
+
+func (c *context) replaceConfigGrp() {
+ opts := os.UpdateOpts{
+ Values: map[string]interface{}{
+ "big_tables": 1,
+ },
+ }
+
+ err := config.Replace(c.client, c.configGroupID, opts).ExtractErr()
+ c.Logf("Replaced values for config group %s", c.configGroupID)
+ c.AssertNoErr(err)
+}
+
+func (c *context) associateInstanceWithConfigGrp() {
+ err := instances.AssociateWithConfigGroup(c.client, c.instanceID, c.configGroupID).ExtractErr()
+ c.Logf("Associated instance %s with config group %s", c.instanceID, c.configGroupID)
+ c.AssertNoErr(err)
+}
+
+func (c *context) listConfigGrpInstances() {
+ c.Logf("Listing all instances associated with config group %s", c.configGroupID)
+
+ err := config.ListInstances(c.client, c.configGroupID).EachPage(func(page pagination.Page) (bool, error) {
+ instanceList, err := instances.ExtractInstances(page)
+ c.AssertNoErr(err)
+
+ for _, instance := range instanceList {
+ c.Logf("Instance: %#v", instance)
+ }
+
+ return true, nil
+ })
+
+ c.AssertNoErr(err)
+}
+
+func (c *context) deleteConfigGrp() {
+ err := config.Delete(c.client, c.configGroupID).ExtractErr()
+ c.Logf("Deleted config group %s", c.configGroupID)
+ c.AssertNoErr(err)
+}
+
+func (c *context) detachInstanceFromGrp() {
+ err := instances.DetachFromConfigGroup(c.client, c.instanceID).ExtractErr()
+ c.Logf("Detached instance %s from config groups", c.instanceID)
+ c.AssertNoErr(err)
+}
diff --git a/acceptance/rackspace/db/v1/database_test.go b/acceptance/rackspace/db/v1/database_test.go
new file mode 100644
index 0000000..d5c448f
--- /dev/null
+++ b/acceptance/rackspace/db/v1/database_test.go
@@ -0,0 +1,54 @@
+// +build acceptance db rackspace
+
+package v1
+
+import (
+ "github.com/rackspace/gophercloud/acceptance/tools"
+ db "github.com/rackspace/gophercloud/openstack/db/v1/databases"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+func (c *context) createDBs() {
+ dbs := []string{
+ tools.RandomString("db_", 5),
+ tools.RandomString("db_", 5),
+ tools.RandomString("db_", 5),
+ }
+
+ opts := db.BatchCreateOpts{
+ db.CreateOpts{Name: dbs[0]},
+ db.CreateOpts{Name: dbs[1]},
+ db.CreateOpts{Name: dbs[2]},
+ }
+
+ err := db.Create(c.client, c.instanceID, opts).ExtractErr()
+ c.Logf("Created three databases on instance %s: %s, %s, %s", c.instanceID, dbs[0], dbs[1], dbs[2])
+ c.AssertNoErr(err)
+
+ c.DBIDs = dbs
+}
+
+func (c *context) listDBs() {
+ c.Logf("Listing databases on instance %s", c.instanceID)
+
+ err := db.List(c.client, c.instanceID).EachPage(func(page pagination.Page) (bool, error) {
+ dbList, err := db.ExtractDBs(page)
+ c.AssertNoErr(err)
+
+ for _, db := range dbList {
+ c.Logf("DB: %#v", db)
+ }
+
+ return true, nil
+ })
+
+ c.AssertNoErr(err)
+}
+
+func (c *context) deleteDBs() {
+ for _, id := range c.DBIDs {
+ err := db.Delete(c.client, c.instanceID, id).ExtractErr()
+ c.AssertNoErr(err)
+ c.Logf("Deleted DB %s", id)
+ }
+}
diff --git a/acceptance/rackspace/db/v1/flavor_test.go b/acceptance/rackspace/db/v1/flavor_test.go
new file mode 100644
index 0000000..0d6e6df
--- /dev/null
+++ b/acceptance/rackspace/db/v1/flavor_test.go
@@ -0,0 +1,32 @@
+// +build acceptance db rackspace
+
+package v1
+
+import (
+ os "github.com/rackspace/gophercloud/openstack/db/v1/flavors"
+ "github.com/rackspace/gophercloud/pagination"
+ "github.com/rackspace/gophercloud/rackspace/db/v1/flavors"
+)
+
+func (c context) listFlavors() {
+ c.Logf("Listing flavors")
+
+ err := flavors.List(c.client).EachPage(func(page pagination.Page) (bool, error) {
+ flavorList, err := os.ExtractFlavors(page)
+ c.AssertNoErr(err)
+
+ for _, f := range flavorList {
+ c.Logf("Flavor: ID [%s] Name [%s] RAM [%d]", f.ID, f.Name, f.RAM)
+ }
+
+ return true, nil
+ })
+
+ c.AssertNoErr(err)
+}
+
+func (c context) getFlavor() {
+ flavor, err := flavors.Get(c.client, "1").Extract()
+ c.Logf("Getting flavor %s", flavor.ID)
+ c.AssertNoErr(err)
+}
diff --git a/acceptance/rackspace/db/v1/instance_test.go b/acceptance/rackspace/db/v1/instance_test.go
new file mode 100644
index 0000000..b5540e3
--- /dev/null
+++ b/acceptance/rackspace/db/v1/instance_test.go
@@ -0,0 +1,169 @@
+// +build acceptance db rackspace
+
+package v1
+
+import (
+ "testing"
+
+ "github.com/rackspace/gophercloud/acceptance/tools"
+ "github.com/rackspace/gophercloud/pagination"
+ "github.com/rackspace/gophercloud/rackspace/db/v1/instances"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestRunner(t *testing.T) {
+ c := newContext(t)
+
+ // FLAVOR tests
+ c.listFlavors()
+ c.getFlavor()
+
+ // INSTANCE tests
+ c.createInstance()
+ c.listInstances()
+ c.getInstance()
+ c.isRootEnabled()
+ c.enableRootUser()
+ c.isRootEnabled()
+ c.restartInstance()
+ c.resizeInstance()
+ c.resizeVol()
+ c.getDefaultConfig()
+
+ // REPLICA tests
+ c.createReplica()
+ c.detachReplica()
+
+ // BACKUP tests
+ c.createBackup()
+ c.getBackup()
+ c.listAllBackups()
+ c.listInstanceBackups()
+ c.deleteBackup()
+
+ // CONFIG GROUP tests
+ c.createConfigGrp()
+ c.getConfigGrp()
+ c.updateConfigGrp()
+ c.replaceConfigGrp()
+ c.associateInstanceWithConfigGrp()
+ c.listConfigGrpInstances()
+ c.detachInstanceFromGrp()
+ c.deleteConfigGrp()
+
+ // DATABASE tests
+ c.createDBs()
+ c.listDBs()
+
+ // USER tests
+ c.createUsers()
+ c.listUsers()
+ c.changeUserPwd()
+ c.getUser()
+ c.updateUser()
+ c.listUserAccess()
+ c.revokeUserAccess()
+ c.grantUserAccess()
+
+ // TEARDOWN
+ c.deleteUsers()
+ c.deleteDBs()
+
+ c.restartInstance()
+ c.WaitUntilActive(c.instanceID)
+
+ c.deleteInstance(c.replicaID)
+ c.deleteInstance(c.instanceID)
+}
+
+func (c *context) createInstance() {
+ opts := instances.CreateOpts{
+ FlavorRef: "1",
+ Size: 1,
+ Name: tools.RandomString("gopher_db", 5),
+ }
+
+ instance, err := instances.Create(c.client, opts).Extract()
+ th.AssertNoErr(c.test, err)
+
+ c.Logf("Creating %s. Waiting...", instance.ID)
+ c.WaitUntilActive(instance.ID)
+ c.Logf("Created instance %s", instance.ID)
+
+ c.instanceID = instance.ID
+}
+
+func (c *context) listInstances() {
+ c.Logf("Listing instances")
+
+ err := instances.List(c.client, nil).EachPage(func(page pagination.Page) (bool, error) {
+ instanceList, err := instances.ExtractInstances(page)
+ c.AssertNoErr(err)
+
+ for _, i := range instanceList {
+ c.Logf("Instance: ID [%s] Name [%s] Status [%s] VolSize [%d] Datastore Type [%s]",
+ i.ID, i.Name, i.Status, i.Volume.Size, i.Datastore.Type)
+ }
+
+ return true, nil
+ })
+
+ c.AssertNoErr(err)
+}
+
+func (c *context) getInstance() {
+ instance, err := instances.Get(c.client, c.instanceID).Extract()
+ c.AssertNoErr(err)
+ c.Logf("Getting instance: %#v", instance)
+}
+
+func (c *context) deleteInstance(id string) {
+ err := instances.Delete(c.client, id).ExtractErr()
+ c.AssertNoErr(err)
+ c.Logf("Deleted instance %s", id)
+}
+
+func (c *context) enableRootUser() {
+ _, err := instances.EnableRootUser(c.client, c.instanceID).Extract()
+ c.AssertNoErr(err)
+ c.Logf("Enabled root user on %s", c.instanceID)
+}
+
+func (c *context) isRootEnabled() {
+ enabled, err := instances.IsRootEnabled(c.client, c.instanceID)
+ c.AssertNoErr(err)
+ c.Logf("Is root enabled? %s", enabled)
+}
+
+func (c *context) restartInstance() {
+ id := c.instanceID
+ err := instances.Restart(c.client, id).ExtractErr()
+ c.AssertNoErr(err)
+ c.Logf("Restarting %s. Waiting...", id)
+ c.WaitUntilActive(id)
+ c.Logf("Restarted %s", id)
+}
+
+func (c *context) resizeInstance() {
+ id := c.instanceID
+ err := instances.Resize(c.client, id, "2").ExtractErr()
+ c.AssertNoErr(err)
+ c.Logf("Resizing %s. Waiting...", id)
+ c.WaitUntilActive(id)
+ c.Logf("Resized %s with flavorRef %s", id, "2")
+}
+
+func (c *context) resizeVol() {
+ id := c.instanceID
+ err := instances.ResizeVolume(c.client, id, 2).ExtractErr()
+ c.AssertNoErr(err)
+ c.Logf("Resizing volume of %s. Waiting...", id)
+ c.WaitUntilActive(id)
+ c.Logf("Resized the volume of %s to %d GB", id, 2)
+}
+
+func (c *context) getDefaultConfig() {
+ config, err := instances.GetDefaultConfig(c.client, c.instanceID).Extract()
+ c.Logf("Default config group for instance %s: %#v", c.instanceID, config)
+ c.AssertNoErr(err)
+}
diff --git a/acceptance/rackspace/db/v1/pkg.go b/acceptance/rackspace/db/v1/pkg.go
new file mode 100644
index 0000000..b7b1f99
--- /dev/null
+++ b/acceptance/rackspace/db/v1/pkg.go
@@ -0,0 +1 @@
+package v1
diff --git a/acceptance/rackspace/db/v1/replica_test.go b/acceptance/rackspace/db/v1/replica_test.go
new file mode 100644
index 0000000..89edf9d
--- /dev/null
+++ b/acceptance/rackspace/db/v1/replica_test.go
@@ -0,0 +1,33 @@
+// +build acceptance db rackspace
+
+package v1
+
+import (
+ "github.com/rackspace/gophercloud/acceptance/tools"
+ "github.com/rackspace/gophercloud/rackspace/db/v1/instances"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func (c *context) createReplica() {
+ opts := instances.CreateOpts{
+ FlavorRef: "2",
+ Size: 1,
+ Name: tools.RandomString("gopher_db", 5),
+ ReplicaOf: c.instanceID,
+ }
+
+ repl, err := instances.Create(c.client, opts).Extract()
+ th.AssertNoErr(c.test, err)
+
+ c.Logf("Creating replica of %s. Waiting...", c.instanceID)
+ c.WaitUntilActive(repl.ID)
+ c.Logf("Created replica %#v", repl)
+
+ c.replicaID = repl.ID
+}
+
+func (c *context) detachReplica() {
+ err := instances.DetachReplica(c.client, c.replicaID).ExtractErr()
+ c.Logf("Detached replica %s", c.replicaID)
+ c.AssertNoErr(err)
+}
diff --git a/acceptance/rackspace/db/v1/user_test.go b/acceptance/rackspace/db/v1/user_test.go
new file mode 100644
index 0000000..0488f5d
--- /dev/null
+++ b/acceptance/rackspace/db/v1/user_test.go
@@ -0,0 +1,125 @@
+// +build acceptance db rackspace
+
+package v1
+
+import (
+ "github.com/rackspace/gophercloud/acceptance/tools"
+ db "github.com/rackspace/gophercloud/openstack/db/v1/databases"
+ os "github.com/rackspace/gophercloud/openstack/db/v1/users"
+ "github.com/rackspace/gophercloud/pagination"
+ "github.com/rackspace/gophercloud/rackspace/db/v1/users"
+)
+
+func (c *context) createUsers() {
+ c.users = []string{
+ tools.RandomString("user_", 5),
+ tools.RandomString("user_", 5),
+ tools.RandomString("user_", 5),
+ }
+
+ db1 := db.CreateOpts{Name: c.DBIDs[0]}
+ db2 := db.CreateOpts{Name: c.DBIDs[1]}
+ db3 := db.CreateOpts{Name: c.DBIDs[2]}
+
+ opts := os.BatchCreateOpts{
+ os.CreateOpts{
+ Name: c.users[0],
+ Password: tools.RandomString("db_", 5),
+ Databases: db.BatchCreateOpts{db1, db2, db3},
+ },
+ os.CreateOpts{
+ Name: c.users[1],
+ Password: tools.RandomString("db_", 5),
+ Databases: db.BatchCreateOpts{db1, db2},
+ },
+ os.CreateOpts{
+ Name: c.users[2],
+ Password: tools.RandomString("db_", 5),
+ Databases: db.BatchCreateOpts{db3},
+ },
+ }
+
+ err := users.Create(c.client, c.instanceID, opts).ExtractErr()
+ c.Logf("Created three users on instance %s: %s, %s, %s", c.instanceID, c.users[0], c.users[1], c.users[2])
+ c.AssertNoErr(err)
+}
+
+func (c *context) listUsers() {
+ c.Logf("Listing users on instance %s", c.instanceID)
+
+ err := os.List(c.client, c.instanceID).EachPage(func(page pagination.Page) (bool, error) {
+ uList, err := os.ExtractUsers(page)
+ c.AssertNoErr(err)
+
+ for _, u := range uList {
+ c.Logf("User: %#v", u)
+ }
+
+ return true, nil
+ })
+
+ c.AssertNoErr(err)
+}
+
+func (c *context) deleteUsers() {
+ for _, id := range c.users {
+ err := users.Delete(c.client, c.instanceID, id).ExtractErr()
+ c.AssertNoErr(err)
+ c.Logf("Deleted user %s", id)
+ }
+}
+
+func (c *context) changeUserPwd() {
+ opts := os.BatchCreateOpts{}
+
+ for _, name := range c.users[:1] {
+ opts = append(opts, os.CreateOpts{Name: name, Password: tools.RandomString("", 5)})
+ }
+
+ err := users.ChangePassword(c.client, c.instanceID, opts).ExtractErr()
+ c.Logf("Updated 2 users' passwords")
+ c.AssertNoErr(err)
+}
+
+func (c *context) getUser() {
+ user, err := users.Get(c.client, c.instanceID, c.users[0]).Extract()
+ c.Logf("Getting user %s", user)
+ c.AssertNoErr(err)
+}
+
+func (c *context) updateUser() {
+ opts := users.UpdateOpts{Name: tools.RandomString("new_name_", 5)}
+ err := users.Update(c.client, c.instanceID, c.users[0], opts).ExtractErr()
+ c.Logf("Updated user %s", c.users[0])
+ c.AssertNoErr(err)
+ c.users[0] = opts.Name
+}
+
+func (c *context) listUserAccess() {
+ err := users.ListAccess(c.client, c.instanceID, c.users[0]).EachPage(func(page pagination.Page) (bool, error) {
+ dbList, err := users.ExtractDBs(page)
+ c.AssertNoErr(err)
+
+ for _, db := range dbList {
+ c.Logf("User %s has access to DB: %#v", db)
+ }
+
+ return true, nil
+ })
+
+ c.AssertNoErr(err)
+}
+
+func (c *context) grantUserAccess() {
+ opts := db.BatchCreateOpts{db.CreateOpts{Name: c.DBIDs[0]}}
+ err := users.GrantAccess(c.client, c.instanceID, c.users[0], opts).ExtractErr()
+ c.Logf("Granted access for user %s to DB %s", c.users[0], c.DBIDs[0])
+ c.AssertNoErr(err)
+}
+
+func (c *context) revokeUserAccess() {
+ dbName, userName := c.DBIDs[0], c.users[0]
+ err := users.RevokeAccess(c.client, c.instanceID, userName, dbName).ExtractErr()
+ c.Logf("Revoked access for user %s to DB %s", userName, dbName)
+ c.AssertNoErr(err)
+}
diff --git a/openstack/client.go b/openstack/client.go
index 1193b19..b30d205 100644
--- a/openstack/client.go
+++ b/openstack/client.go
@@ -261,3 +261,13 @@
}
return &gophercloud.ServiceClient{ProviderClient: client, Endpoint: url}, nil
}
+
+// NewDBV1 creates a ServiceClient that may be used to access the v1 DB service.
+func NewDBV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
+ eo.ApplyDefaults("database")
+ url, err := client.EndpointLocator(eo)
+ if err != nil {
+ return nil, err
+ }
+ return &gophercloud.ServiceClient{ProviderClient: client, Endpoint: url}, nil
+}
diff --git a/openstack/db/v1/configurations/doc.go b/openstack/db/v1/configurations/doc.go
new file mode 100644
index 0000000..45b9cfb
--- /dev/null
+++ b/openstack/db/v1/configurations/doc.go
@@ -0,0 +1,11 @@
+// Package configurations provides information and interaction with the
+// configuration API resource in the Rackspace Database service.
+//
+// A configuration group is a collection of key/value pairs which define how a
+// particular database operates. These key/value pairs are specific to each
+// datastore type and serve like settings. Some directives are capable of being
+// applied dynamically, while other directives require a server restart to take
+// effect. The configuration group can be applied to an instance at creation or
+// applied to an existing instance to modify the behavior of the running
+// datastore on the instance.
+package configurations
diff --git a/openstack/db/v1/configurations/fixtures.go b/openstack/db/v1/configurations/fixtures.go
new file mode 100644
index 0000000..ae65416
--- /dev/null
+++ b/openstack/db/v1/configurations/fixtures.go
@@ -0,0 +1,157 @@
+package configurations
+
+import (
+ "fmt"
+ "time"
+)
+
+var (
+ timestamp = "2015-11-12T14:22:42Z"
+ timeVal, _ = time.Parse(time.RFC3339, timestamp)
+)
+
+var singleConfigJSON = `
+{
+ "created": "` + timestamp + `",
+ "datastore_name": "mysql",
+ "datastore_version_id": "b00000b0-00b0-0b00-00b0-000b000000bb",
+ "datastore_version_name": "5.6",
+ "description": "example_description",
+ "id": "005a8bb7-a8df-40ee-b0b7-fc144641abc2",
+ "name": "example-configuration-name",
+ "updated": "` + timestamp + `"
+}
+`
+
+var singleConfigWithValuesJSON = `
+{
+ "created": "` + timestamp + `",
+ "datastore_name": "mysql",
+ "datastore_version_id": "b00000b0-00b0-0b00-00b0-000b000000bb",
+ "datastore_version_name": "5.6",
+ "description": "example description",
+ "id": "005a8bb7-a8df-40ee-b0b7-fc144641abc2",
+ "instance_count": 0,
+ "name": "example-configuration-name",
+ "updated": "` + timestamp + `",
+ "values": {
+ "collation_server": "latin1_swedish_ci",
+ "connect_timeout": 120
+ }
+}
+`
+
+var (
+ ListConfigsJSON = fmt.Sprintf(`{"configurations": [%s]}`, singleConfigJSON)
+ GetConfigJSON = fmt.Sprintf(`{"configuration": %s}`, singleConfigJSON)
+ CreateConfigJSON = fmt.Sprintf(`{"configuration": %s}`, singleConfigWithValuesJSON)
+)
+
+var CreateReq = `
+{
+ "configuration": {
+ "datastore": {
+ "type": "a00000a0-00a0-0a00-00a0-000a000000aa",
+ "version": "b00000b0-00b0-0b00-00b0-000b000000bb"
+ },
+ "description": "example description",
+ "name": "example-configuration-name",
+ "values": {
+ "collation_server": "latin1_swedish_ci",
+ "connect_timeout": 120
+ }
+ }
+}
+`
+
+var UpdateReq = `
+{
+ "configuration": {
+ "values": {
+ "connect_timeout": 300
+ }
+ }
+}
+`
+
+var ListInstancesJSON = `
+{
+ "instances": [
+ {
+ "id": "d4603f69-ec7e-4e9b-803f-600b9205576f",
+ "name": "json_rack_instance"
+ }
+ ]
+}
+`
+
+var ListParamsJSON = `
+{
+ "configuration-parameters": [
+ {
+ "max": 1,
+ "min": 0,
+ "name": "innodb_file_per_table",
+ "restart_required": true,
+ "type": "integer"
+ },
+ {
+ "max": 4294967296,
+ "min": 0,
+ "name": "key_buffer_size",
+ "restart_required": false,
+ "type": "integer"
+ },
+ {
+ "max": 65535,
+ "min": 2,
+ "name": "connect_timeout",
+ "restart_required": false,
+ "type": "integer"
+ },
+ {
+ "max": 4294967296,
+ "min": 0,
+ "name": "join_buffer_size",
+ "restart_required": false,
+ "type": "integer"
+ }
+ ]
+}
+`
+
+var GetParamJSON = `
+{
+ "max": 1,
+ "min": 0,
+ "name": "innodb_file_per_table",
+ "restart_required": true,
+ "type": "integer"
+}
+`
+
+var ExampleConfig = Config{
+ Created: timeVal,
+ DatastoreName: "mysql",
+ DatastoreVersionID: "b00000b0-00b0-0b00-00b0-000b000000bb",
+ DatastoreVersionName: "5.6",
+ Description: "example_description",
+ ID: "005a8bb7-a8df-40ee-b0b7-fc144641abc2",
+ Name: "example-configuration-name",
+ Updated: timeVal,
+}
+
+var ExampleConfigWithValues = Config{
+ Created: timeVal,
+ DatastoreName: "mysql",
+ DatastoreVersionID: "b00000b0-00b0-0b00-00b0-000b000000bb",
+ DatastoreVersionName: "5.6",
+ Description: "example description",
+ ID: "005a8bb7-a8df-40ee-b0b7-fc144641abc2",
+ Name: "example-configuration-name",
+ Updated: timeVal,
+ Values: map[string]interface{}{
+ "collation_server": "latin1_swedish_ci",
+ "connect_timeout": 120,
+ },
+}
diff --git a/openstack/db/v1/configurations/requests.go b/openstack/db/v1/configurations/requests.go
new file mode 100644
index 0000000..83c7102
--- /dev/null
+++ b/openstack/db/v1/configurations/requests.go
@@ -0,0 +1,287 @@
+package configurations
+
+import (
+ "errors"
+
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/openstack/db/v1/instances"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+// List will list all of the available configurations.
+func List(client *gophercloud.ServiceClient) pagination.Pager {
+ pageFn := func(r pagination.PageResult) pagination.Page {
+ return ConfigPage{pagination.SinglePageBase(r)}
+ }
+
+ return pagination.NewPager(client, baseURL(client), pageFn)
+}
+
+// CreateOptsBuilder is a top-level interface which renders a JSON map.
+type CreateOptsBuilder interface {
+ ToConfigCreateMap() (map[string]interface{}, error)
+}
+
+// DatastoreOpts is the primary options struct for creating and modifying
+// how configuration resources are associated with datastores.
+type DatastoreOpts struct {
+ // [OPTIONAL] The type of datastore. Defaults to "MySQL".
+ Type string
+
+ // [OPTIONAL] The specific version of a datastore. Defaults to "5.6".
+ Version string
+}
+
+// ToMap renders a JSON map for a datastore setting.
+func (opts DatastoreOpts) ToMap() (map[string]string, error) {
+ datastore := map[string]string{}
+
+ if opts.Type != "" {
+ datastore["type"] = opts.Type
+ }
+
+ if opts.Version != "" {
+ datastore["version"] = opts.Version
+ }
+
+ return datastore, nil
+}
+
+// CreateOpts is the struct responsible for configuring new configurations.
+type CreateOpts struct {
+ // [REQUIRED] The configuration group name
+ Name string
+
+ // [REQUIRED] A map of user-defined configuration settings that will define
+ // how each associated datastore works. Each key/value pair is specific to a
+ // datastore type.
+ Values map[string]interface{}
+
+ // [OPTIONAL] Associates the configuration group with a particular datastore.
+ Datastore *DatastoreOpts
+
+ // [OPTIONAL] A human-readable explanation for the group.
+ Description string
+}
+
+// ToConfigCreateMap casts a CreateOpts struct into a JSON map.
+func (opts CreateOpts) ToConfigCreateMap() (map[string]interface{}, error) {
+ if opts.Name == "" {
+ return nil, errors.New("Name is a required field")
+ }
+ if len(opts.Values) == 0 {
+ return nil, errors.New("Values must be a populated map")
+ }
+
+ config := map[string]interface{}{
+ "name": opts.Name,
+ "values": opts.Values,
+ }
+
+ if opts.Datastore != nil {
+ ds, err := opts.Datastore.ToMap()
+ if err != nil {
+ return config, err
+ }
+ config["datastore"] = ds
+ }
+
+ if opts.Description != "" {
+ config["description"] = opts.Description
+ }
+
+ return map[string]interface{}{"configuration": config}, nil
+}
+
+// Create will create a new configuration group.
+func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) CreateResult {
+ var res CreateResult
+
+ reqBody, err := opts.ToConfigCreateMap()
+ if err != nil {
+ res.Err = err
+ return res
+ }
+
+ _, res.Err = client.Request("POST", baseURL(client), gophercloud.RequestOpts{
+ OkCodes: []int{200},
+ JSONBody: &reqBody,
+ JSONResponse: &res.Body,
+ })
+
+ return res
+}
+
+// Get will retrieve the details for a specified configuration group.
+func Get(client *gophercloud.ServiceClient, configID string) GetResult {
+ var res GetResult
+
+ _, res.Err = client.Request("GET", resourceURL(client, configID), gophercloud.RequestOpts{
+ OkCodes: []int{200},
+ JSONResponse: &res.Body,
+ })
+
+ return res
+}
+
+// UpdateOptsBuilder is the top-level interface for casting update options into
+// JSON maps.
+type UpdateOptsBuilder interface {
+ ToConfigUpdateMap() (map[string]interface{}, error)
+}
+
+// UpdateOpts is the struct responsible for modifying existing configurations.
+type UpdateOpts struct {
+ // [OPTIONAL] The configuration group name
+ Name string
+
+ // [OPTIONAL] A map of user-defined configuration settings that will define
+ // how each associated datastore works. Each key/value pair is specific to a
+ // datastore type.
+ Values map[string]interface{}
+
+ // [OPTIONAL] Associates the configuration group with a particular datastore.
+ Datastore *DatastoreOpts
+
+ // [OPTIONAL] A human-readable explanation for the group.
+ Description string
+}
+
+// ToConfigUpdateMap will cast an UpdateOpts struct into a JSON map.
+func (opts UpdateOpts) ToConfigUpdateMap() (map[string]interface{}, error) {
+ config := map[string]interface{}{}
+
+ if opts.Name != "" {
+ config["name"] = opts.Name
+ }
+
+ if opts.Description != "" {
+ config["description"] = opts.Description
+ }
+
+ if opts.Datastore != nil {
+ ds, err := opts.Datastore.ToMap()
+ if err != nil {
+ return config, err
+ }
+ config["datastore"] = ds
+ }
+
+ if len(opts.Values) > 0 {
+ config["values"] = opts.Values
+ }
+
+ return map[string]interface{}{"configuration": config}, nil
+}
+
+// Update will modify an existing configuration group by performing a merge
+// between new and existing values. If the key already exists, the new value
+// will overwrite. All other keys will remain unaffected.
+func Update(client *gophercloud.ServiceClient, configID string, opts UpdateOptsBuilder) UpdateResult {
+ var res UpdateResult
+
+ reqBody, err := opts.ToConfigUpdateMap()
+ if err != nil {
+ res.Err = err
+ return res
+ }
+
+ _, res.Err = client.Request("PATCH", resourceURL(client, configID), gophercloud.RequestOpts{
+ OkCodes: []int{200},
+ JSONBody: &reqBody,
+ })
+
+ return res
+}
+
+// Replace will modify an existing configuration group by overwriting the
+// entire parameter group with the new values provided. Any existing keys not
+// included in UpdateOptsBuilder will be deleted.
+func Replace(client *gophercloud.ServiceClient, configID string, opts UpdateOptsBuilder) ReplaceResult {
+ var res ReplaceResult
+
+ reqBody, err := opts.ToConfigUpdateMap()
+ if err != nil {
+ res.Err = err
+ return res
+ }
+
+ _, res.Err = client.Request("PUT", resourceURL(client, configID), gophercloud.RequestOpts{
+ OkCodes: []int{202},
+ JSONBody: &reqBody,
+ })
+
+ return res
+}
+
+// Delete will permanently delete a configuration group. Please note that
+// config groups cannot be deleted whilst still attached to running instances -
+// you must detach and then delete them.
+func Delete(client *gophercloud.ServiceClient, configID string) DeleteResult {
+ var res DeleteResult
+
+ _, res.Err = client.Request("DELETE", resourceURL(client, configID), gophercloud.RequestOpts{
+ OkCodes: []int{202},
+ })
+
+ return res
+}
+
+// ListInstances will list all the instances associated with a particular
+// configuration group.
+func ListInstances(client *gophercloud.ServiceClient, configID string) pagination.Pager {
+ pageFn := func(r pagination.PageResult) pagination.Page {
+ return instances.InstancePage{pagination.LinkedPageBase{PageResult: r}}
+ }
+ return pagination.NewPager(client, instancesURL(client, configID), pageFn)
+}
+
+// ListDatastoreParams will list all the available and supported parameters
+// that can be used for a particular datastore ID and a particular version.
+// For example, if you are wondering how you can configure a MySQL 5.6 instance,
+// you can use this operation (you will need to retrieve the MySQL datastore ID
+// by using the datastores API).
+func ListDatastoreParams(client *gophercloud.ServiceClient, datastoreID, versionID string) pagination.Pager {
+ pageFn := func(r pagination.PageResult) pagination.Page {
+ return ParamPage{pagination.SinglePageBase(r)}
+ }
+ return pagination.NewPager(client, listDSParamsURL(client, datastoreID, versionID), pageFn)
+}
+
+// GetDatastoreParam will retrieve information about a specific configuration
+// parameter. For example, you can use this operation to understand more about
+// "innodb_file_per_table" configuration param for MySQL datastores. You will
+// need the param's ID first, which can be attained by using the ListDatastoreParams
+// operation.
+func GetDatastoreParam(client *gophercloud.ServiceClient, datastoreID, versionID, paramID string) ParamResult {
+ var res ParamResult
+
+ _, res.Err = client.Request("GET", getDSParamURL(client, datastoreID, versionID, paramID), gophercloud.RequestOpts{
+ OkCodes: []int{200},
+ JSONResponse: &res.Body,
+ })
+
+ return res
+}
+
+// ListGlobalParams is similar to ListDatastoreParams but does not require a
+// DatastoreID.
+func ListGlobalParams(client *gophercloud.ServiceClient, versionID string) pagination.Pager {
+ pageFn := func(r pagination.PageResult) pagination.Page {
+ return ParamPage{pagination.SinglePageBase(r)}
+ }
+ return pagination.NewPager(client, listGlobalParamsURL(client, versionID), pageFn)
+}
+
+// GetGlobalParam is similar to GetDatastoreParam but does not require a
+// DatastoreID.
+func GetGlobalParam(client *gophercloud.ServiceClient, versionID, paramID string) ParamResult {
+ var res ParamResult
+
+ _, res.Err = client.Request("GET", getGlobalParamURL(client, versionID, paramID), gophercloud.RequestOpts{
+ OkCodes: []int{200},
+ JSONResponse: &res.Body,
+ })
+
+ return res
+}
diff --git a/openstack/db/v1/configurations/requests_test.go b/openstack/db/v1/configurations/requests_test.go
new file mode 100644
index 0000000..db66f29
--- /dev/null
+++ b/openstack/db/v1/configurations/requests_test.go
@@ -0,0 +1,236 @@
+package configurations
+
+import (
+ "testing"
+
+ "github.com/rackspace/gophercloud/pagination"
+ "github.com/rackspace/gophercloud/rackspace/db/v1/instances"
+ th "github.com/rackspace/gophercloud/testhelper"
+ fake "github.com/rackspace/gophercloud/testhelper/client"
+ "github.com/rackspace/gophercloud/testhelper/fixture"
+)
+
+var (
+ configID = "{configID}"
+ _baseURL = "/configurations"
+ resURL = _baseURL + "/" + configID
+
+ dsID = "{datastoreID}"
+ versionID = "{versionID}"
+ paramID = "{paramID}"
+ dsParamListURL = "/datastores/" + dsID + "/versions/" + versionID + "/parameters"
+ dsParamGetURL = "/datastores/" + dsID + "/versions/" + versionID + "/parameters/" + paramID
+ globalParamListURL = "/datastores/versions/" + versionID + "/parameters"
+ globalParamGetURL = "/datastores/versions/" + versionID + "/parameters/" + paramID
+)
+
+func TestList(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ fixture.SetupHandler(t, _baseURL, "GET", "", ListConfigsJSON, 200)
+
+ count := 0
+ err := List(fake.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) {
+ count++
+ actual, err := ExtractConfigs(page)
+ th.AssertNoErr(t, err)
+
+ expected := []Config{ExampleConfig}
+ th.AssertDeepEquals(t, expected, actual)
+
+ return true, nil
+ })
+
+ th.AssertEquals(t, 1, count)
+ th.AssertNoErr(t, err)
+}
+
+func TestGet(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ fixture.SetupHandler(t, resURL, "GET", "", GetConfigJSON, 200)
+
+ config, err := Get(fake.ServiceClient(), configID).Extract()
+ th.AssertNoErr(t, err)
+ th.AssertDeepEquals(t, &ExampleConfig, config)
+}
+
+func TestCreate(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ fixture.SetupHandler(t, _baseURL, "POST", CreateReq, CreateConfigJSON, 200)
+
+ opts := CreateOpts{
+ Datastore: &DatastoreOpts{
+ Type: "a00000a0-00a0-0a00-00a0-000a000000aa",
+ Version: "b00000b0-00b0-0b00-00b0-000b000000bb",
+ },
+ Description: "example description",
+ Name: "example-configuration-name",
+ Values: map[string]interface{}{
+ "collation_server": "latin1_swedish_ci",
+ "connect_timeout": 120,
+ },
+ }
+
+ config, err := Create(fake.ServiceClient(), opts).Extract()
+ th.AssertNoErr(t, err)
+ th.AssertDeepEquals(t, &ExampleConfigWithValues, config)
+}
+
+func TestUpdate(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ fixture.SetupHandler(t, resURL, "PATCH", UpdateReq, "", 200)
+
+ opts := UpdateOpts{
+ Values: map[string]interface{}{
+ "connect_timeout": 300,
+ },
+ }
+
+ err := Update(fake.ServiceClient(), configID, opts).ExtractErr()
+ th.AssertNoErr(t, err)
+}
+
+func TestReplace(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ fixture.SetupHandler(t, resURL, "PUT", UpdateReq, "", 202)
+
+ opts := UpdateOpts{
+ Values: map[string]interface{}{
+ "connect_timeout": 300,
+ },
+ }
+
+ err := Replace(fake.ServiceClient(), configID, opts).ExtractErr()
+ th.AssertNoErr(t, err)
+}
+
+func TestDelete(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ fixture.SetupHandler(t, resURL, "DELETE", "", "", 202)
+
+ err := Delete(fake.ServiceClient(), configID).ExtractErr()
+ th.AssertNoErr(t, err)
+}
+
+func TestListInstances(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ fixture.SetupHandler(t, resURL+"/instances", "GET", "", ListInstancesJSON, 200)
+
+ expectedInstance := instances.Instance{
+ ID: "d4603f69-ec7e-4e9b-803f-600b9205576f",
+ Name: "json_rack_instance",
+ }
+
+ pages := 0
+ err := ListInstances(fake.ServiceClient(), configID).EachPage(func(page pagination.Page) (bool, error) {
+ pages++
+
+ actual, err := instances.ExtractInstances(page)
+ if err != nil {
+ return false, err
+ }
+
+ th.AssertDeepEquals(t, actual, []instances.Instance{expectedInstance})
+
+ return true, nil
+ })
+
+ th.AssertNoErr(t, err)
+ th.AssertEquals(t, 1, pages)
+}
+
+func TestListDSParams(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ fixture.SetupHandler(t, dsParamListURL, "GET", "", ListParamsJSON, 200)
+
+ pages := 0
+ err := ListDatastoreParams(fake.ServiceClient(), dsID, versionID).EachPage(func(page pagination.Page) (bool, error) {
+ pages++
+
+ actual, err := ExtractParams(page)
+ if err != nil {
+ return false, err
+ }
+
+ expected := []Param{
+ Param{Max: 1, Min: 0, Name: "innodb_file_per_table", RestartRequired: true, Type: "integer"},
+ Param{Max: 4294967296, Min: 0, Name: "key_buffer_size", RestartRequired: false, Type: "integer"},
+ Param{Max: 65535, Min: 2, Name: "connect_timeout", RestartRequired: false, Type: "integer"},
+ Param{Max: 4294967296, Min: 0, Name: "join_buffer_size", RestartRequired: false, Type: "integer"},
+ }
+
+ th.AssertDeepEquals(t, actual, expected)
+
+ return true, nil
+ })
+
+ th.AssertNoErr(t, err)
+ th.AssertEquals(t, 1, pages)
+}
+
+func TestGetDSParam(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ fixture.SetupHandler(t, dsParamGetURL, "GET", "", GetParamJSON, 200)
+
+ param, err := GetDatastoreParam(fake.ServiceClient(), dsID, versionID, paramID).Extract()
+ th.AssertNoErr(t, err)
+
+ expected := &Param{
+ Max: 1, Min: 0, Name: "innodb_file_per_table", RestartRequired: true, Type: "integer",
+ }
+
+ th.AssertDeepEquals(t, expected, param)
+}
+
+func TestListGlobalParams(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ fixture.SetupHandler(t, globalParamListURL, "GET", "", ListParamsJSON, 200)
+
+ pages := 0
+ err := ListGlobalParams(fake.ServiceClient(), versionID).EachPage(func(page pagination.Page) (bool, error) {
+ pages++
+
+ actual, err := ExtractParams(page)
+ if err != nil {
+ return false, err
+ }
+
+ expected := []Param{
+ Param{Max: 1, Min: 0, Name: "innodb_file_per_table", RestartRequired: true, Type: "integer"},
+ Param{Max: 4294967296, Min: 0, Name: "key_buffer_size", RestartRequired: false, Type: "integer"},
+ Param{Max: 65535, Min: 2, Name: "connect_timeout", RestartRequired: false, Type: "integer"},
+ Param{Max: 4294967296, Min: 0, Name: "join_buffer_size", RestartRequired: false, Type: "integer"},
+ }
+
+ th.AssertDeepEquals(t, actual, expected)
+
+ return true, nil
+ })
+
+ th.AssertNoErr(t, err)
+ th.AssertEquals(t, 1, pages)
+}
+
+func TestGetGlobalParam(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ fixture.SetupHandler(t, globalParamGetURL, "GET", "", GetParamJSON, 200)
+
+ param, err := GetGlobalParam(fake.ServiceClient(), versionID, paramID).Extract()
+ th.AssertNoErr(t, err)
+
+ expected := &Param{
+ Max: 1, Min: 0, Name: "innodb_file_per_table", RestartRequired: true, Type: "integer",
+ }
+
+ th.AssertDeepEquals(t, expected, param)
+}
diff --git a/openstack/db/v1/configurations/results.go b/openstack/db/v1/configurations/results.go
new file mode 100644
index 0000000..37a77ad
--- /dev/null
+++ b/openstack/db/v1/configurations/results.go
@@ -0,0 +1,189 @@
+package configurations
+
+import (
+ "time"
+
+ "github.com/mitchellh/mapstructure"
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+// Config represents a configuration group API resource.
+type Config struct {
+ Created time.Time `mapstructure:"-"`
+ Updated time.Time `mapstructure:"-"`
+ DatastoreName string `mapstructure:"datastore_name"`
+ DatastoreVersionID string `mapstructure:"datastore_version_id"`
+ DatastoreVersionName string `mapstructure:"datastore_version_name"`
+ Description string
+ ID string
+ Name string
+ Values map[string]interface{}
+}
+
+// ConfigPage contains a page of Config resources in a paginated collection.
+type ConfigPage struct {
+ pagination.SinglePageBase
+}
+
+// IsEmpty indicates whether a ConfigPage is empty.
+func (r ConfigPage) IsEmpty() (bool, error) {
+ is, err := ExtractConfigs(r)
+ if err != nil {
+ return true, err
+ }
+ return len(is) == 0, nil
+}
+
+// ExtractConfigs will retrieve a slice of Config structs from a page.
+func ExtractConfigs(page pagination.Page) ([]Config, error) {
+ casted := page.(ConfigPage).Body
+
+ var resp struct {
+ Configs []Config `mapstructure:"configurations" json:"configurations"`
+ }
+
+ err := mapstructure.Decode(casted, &resp)
+
+ var vals []interface{}
+ switch (casted).(type) {
+ case interface{}:
+ vals = casted.(map[string]interface{})["configurations"].([]interface{})
+ }
+
+ for i, v := range vals {
+ val := v.(map[string]interface{})
+
+ if t, ok := val["created"].(string); ok && t != "" {
+ creationTime, err := time.Parse(time.RFC3339, t)
+ if err != nil {
+ return resp.Configs, err
+ }
+ resp.Configs[i].Created = creationTime
+ }
+
+ if t, ok := val["updated"].(string); ok && t != "" {
+ updatedTime, err := time.Parse(time.RFC3339, t)
+ if err != nil {
+ return resp.Configs, err
+ }
+ resp.Configs[i].Updated = updatedTime
+ }
+ }
+
+ return resp.Configs, err
+}
+
+type commonResult struct {
+ gophercloud.Result
+}
+
+// Extract will retrieve a Config resource from an operation result.
+func (r commonResult) Extract() (*Config, error) {
+ if r.Err != nil {
+ return nil, r.Err
+ }
+
+ var response struct {
+ Config Config `mapstructure:"configuration"`
+ }
+
+ err := mapstructure.Decode(r.Body, &response)
+ val := r.Body.(map[string]interface{})["configuration"].(map[string]interface{})
+
+ if t, ok := val["created"].(string); ok && t != "" {
+ creationTime, err := time.Parse(time.RFC3339, t)
+ if err != nil {
+ return &response.Config, err
+ }
+ response.Config.Created = creationTime
+ }
+
+ if t, ok := val["updated"].(string); ok && t != "" {
+ updatedTime, err := time.Parse(time.RFC3339, t)
+ if err != nil {
+ return &response.Config, err
+ }
+ response.Config.Updated = updatedTime
+ }
+
+ return &response.Config, err
+}
+
+// GetResult represents the result of a Get operation.
+type GetResult struct {
+ commonResult
+}
+
+// CreateResult represents the result of a Create operation.
+type CreateResult struct {
+ commonResult
+}
+
+// UpdateResult represents the result of an Update operation.
+type UpdateResult struct {
+ gophercloud.ErrResult
+}
+
+// ReplaceResult represents the result of a Replace operation.
+type ReplaceResult struct {
+ gophercloud.ErrResult
+}
+
+// DeleteResult represents the result of a Delete operation.
+type DeleteResult struct {
+ gophercloud.ErrResult
+}
+
+// Param represents a configuration parameter API resource.
+type Param struct {
+ Max int
+ Min int
+ Name string
+ RestartRequired bool `mapstructure:"restart_required" json:"restart_required"`
+ Type string
+}
+
+// ParamPage contains a page of Param resources in a paginated collection.
+type ParamPage struct {
+ pagination.SinglePageBase
+}
+
+// IsEmpty indicates whether a ParamPage is empty.
+func (r ParamPage) IsEmpty() (bool, error) {
+ is, err := ExtractParams(r)
+ if err != nil {
+ return true, err
+ }
+ return len(is) == 0, nil
+}
+
+// ExtractParams will retrieve a slice of Param structs from a page.
+func ExtractParams(page pagination.Page) ([]Param, error) {
+ casted := page.(ParamPage).Body
+
+ var resp struct {
+ Params []Param `mapstructure:"configuration-parameters" json:"configuration-parameters"`
+ }
+
+ err := mapstructure.Decode(casted, &resp)
+ return resp.Params, err
+}
+
+// ParamResult represents the result of an operation which retrieves details
+// about a particular configuration param.
+type ParamResult struct {
+ gophercloud.Result
+}
+
+// Extract will retrieve a param from an operation result.
+func (r ParamResult) Extract() (*Param, error) {
+ if r.Err != nil {
+ return nil, r.Err
+ }
+
+ var param Param
+
+ err := mapstructure.Decode(r.Body, ¶m)
+ return ¶m, err
+}
diff --git a/openstack/db/v1/configurations/urls.go b/openstack/db/v1/configurations/urls.go
new file mode 100644
index 0000000..abea961
--- /dev/null
+++ b/openstack/db/v1/configurations/urls.go
@@ -0,0 +1,31 @@
+package configurations
+
+import "github.com/rackspace/gophercloud"
+
+func baseURL(c *gophercloud.ServiceClient) string {
+ return c.ServiceURL("configurations")
+}
+
+func resourceURL(c *gophercloud.ServiceClient, configID string) string {
+ return c.ServiceURL("configurations", configID)
+}
+
+func instancesURL(c *gophercloud.ServiceClient, configID string) string {
+ return c.ServiceURL("configurations", configID, "instances")
+}
+
+func listDSParamsURL(c *gophercloud.ServiceClient, datastoreID, versionID string) string {
+ return c.ServiceURL("datastores", datastoreID, "versions", versionID, "parameters")
+}
+
+func getDSParamURL(c *gophercloud.ServiceClient, datastoreID, versionID, paramID string) string {
+ return c.ServiceURL("datastores", datastoreID, "versions", versionID, "parameters", paramID)
+}
+
+func listGlobalParamsURL(c *gophercloud.ServiceClient, versionID string) string {
+ return c.ServiceURL("datastores", "versions", versionID, "parameters")
+}
+
+func getGlobalParamURL(c *gophercloud.ServiceClient, versionID, paramID string) string {
+ return c.ServiceURL("datastores", "versions", versionID, "parameters", paramID)
+}
diff --git a/openstack/db/v1/databases/doc.go b/openstack/db/v1/databases/doc.go
new file mode 100644
index 0000000..15275fe
--- /dev/null
+++ b/openstack/db/v1/databases/doc.go
@@ -0,0 +1,6 @@
+// Package flavors provides information and interaction with the database API
+// resource in the OpenStack Database service.
+//
+// A database, when referred to here, refers to the database engine running on
+// an instance.
+package databases
diff --git a/openstack/db/v1/databases/fixtures.go b/openstack/db/v1/databases/fixtures.go
new file mode 100644
index 0000000..3e67721
--- /dev/null
+++ b/openstack/db/v1/databases/fixtures.go
@@ -0,0 +1,61 @@
+package databases
+
+import (
+ "testing"
+
+ "github.com/rackspace/gophercloud/testhelper/fixture"
+)
+
+var (
+ instanceID = "{instanceID}"
+ resURL = "/instances/" + instanceID + "/databases"
+)
+
+var createDBsReq = `
+{
+ "databases": [
+ {
+ "character_set": "utf8",
+ "collate": "utf8_general_ci",
+ "name": "testingdb"
+ },
+ {
+ "name": "sampledb"
+ }
+ ]
+}
+`
+
+var listDBsResp = `
+{
+ "databases": [
+ {
+ "name": "anotherexampledb"
+ },
+ {
+ "name": "exampledb"
+ },
+ {
+ "name": "nextround"
+ },
+ {
+ "name": "sampledb"
+ },
+ {
+ "name": "testingdb"
+ }
+ ]
+}
+`
+
+func HandleCreate(t *testing.T) {
+ fixture.SetupHandler(t, resURL, "POST", createDBsReq, "", 202)
+}
+
+func HandleList(t *testing.T) {
+ fixture.SetupHandler(t, resURL, "GET", "", listDBsResp, 200)
+}
+
+func HandleDelete(t *testing.T) {
+ fixture.SetupHandler(t, resURL+"/{dbName}", "DELETE", "", "", 202)
+}
diff --git a/openstack/db/v1/databases/requests.go b/openstack/db/v1/databases/requests.go
new file mode 100644
index 0000000..f1eb5d9
--- /dev/null
+++ b/openstack/db/v1/databases/requests.go
@@ -0,0 +1,115 @@
+package databases
+
+import (
+ "fmt"
+
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+// CreateOptsBuilder builds create options
+type CreateOptsBuilder interface {
+ ToDBCreateMap() (map[string]interface{}, error)
+}
+
+// DatabaseOpts is the struct responsible for configuring a database; often in
+// the context of an instance.
+type CreateOpts struct {
+ // [REQUIRED] Specifies the name of the database. Valid names can be composed
+ // of the following characters: letters (either case); numbers; these
+ // characters '@', '?', '#', ' ' but NEVER beginning a name string; '_' is
+ // permitted anywhere. Prohibited characters that are forbidden include:
+ // single quotes, double quotes, back quotes, semicolons, commas, backslashes,
+ // and forward slashes.
+ Name string
+
+ // [OPTIONAL] Set of symbols and encodings. The default character set is
+ // "utf8". See http://dev.mysql.com/doc/refman/5.1/en/charset-mysql.html for
+ // supported character sets.
+ CharSet string
+
+ // [OPTIONAL] Set of rules for comparing characters in a character set. The
+ // default value for collate is "utf8_general_ci". See
+ // http://dev.mysql.com/doc/refman/5.1/en/charset-mysql.html for supported
+ // collations.
+ Collate string
+}
+
+// ToMap is a helper function to convert individual DB create opt structures
+// into sub-maps.
+func (opts CreateOpts) ToMap() (map[string]string, error) {
+ if opts.Name == "" {
+ return nil, fmt.Errorf("Name is a required field")
+ }
+ if len(opts.Name) > 64 {
+ return nil, fmt.Errorf("Name must be less than 64 chars long")
+ }
+
+ db := map[string]string{"name": opts.Name}
+
+ if opts.CharSet != "" {
+ db["character_set"] = opts.CharSet
+ }
+ if opts.Collate != "" {
+ db["collate"] = opts.Collate
+ }
+ return db, nil
+}
+
+// BatchCreateOpts allows for multiple databases to created and modified.
+type BatchCreateOpts []CreateOpts
+
+// ToDBCreateMap renders a JSON map for creating DBs.
+func (opts BatchCreateOpts) ToDBCreateMap() (map[string]interface{}, error) {
+ dbs := make([]map[string]string, len(opts))
+ for i, db := range opts {
+ dbMap, err := db.ToMap()
+ if err != nil {
+ return nil, err
+ }
+ dbs[i] = dbMap
+ }
+ return map[string]interface{}{"databases": dbs}, nil
+}
+
+// Create will create a new database within the specified instance. If the
+// specified instance does not exist, a 404 error will be returned.
+func Create(client *gophercloud.ServiceClient, instanceID string, opts CreateOptsBuilder) CreateResult {
+ var res CreateResult
+
+ reqBody, err := opts.ToDBCreateMap()
+ if err != nil {
+ res.Err = err
+ return res
+ }
+
+ _, res.Err = client.Request("POST", baseURL(client, instanceID), gophercloud.RequestOpts{
+ JSONBody: &reqBody,
+ OkCodes: []int{202},
+ })
+
+ return res
+}
+
+// List will list all of the databases for a specified instance. Note: this
+// operation will only return user-defined databases; it will exclude system
+// databases like "mysql", "information_schema", "lost+found" etc.
+func List(client *gophercloud.ServiceClient, instanceID string) pagination.Pager {
+ createPageFn := func(r pagination.PageResult) pagination.Page {
+ return DBPage{pagination.LinkedPageBase{PageResult: r}}
+ }
+
+ return pagination.NewPager(client, baseURL(client, instanceID), createPageFn)
+}
+
+// Delete will permanently delete the database within a specified instance.
+// All contained data inside the database will also be permanently deleted.
+func Delete(client *gophercloud.ServiceClient, instanceID, dbName string) DeleteResult {
+ var res DeleteResult
+
+ _, res.Err = client.Request("DELETE", dbURL(client, instanceID, dbName), gophercloud.RequestOpts{
+ OkCodes: []int{202},
+ })
+
+ return res
+}
diff --git a/openstack/db/v1/databases/requests_test.go b/openstack/db/v1/databases/requests_test.go
new file mode 100644
index 0000000..8a1b297
--- /dev/null
+++ b/openstack/db/v1/databases/requests_test.go
@@ -0,0 +1,66 @@
+package databases
+
+import (
+ "testing"
+
+ "github.com/rackspace/gophercloud/pagination"
+ th "github.com/rackspace/gophercloud/testhelper"
+ fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestCreate(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ HandleCreate(t)
+
+ opts := BatchCreateOpts{
+ CreateOpts{Name: "testingdb", CharSet: "utf8", Collate: "utf8_general_ci"},
+ CreateOpts{Name: "sampledb"},
+ }
+
+ res := Create(fake.ServiceClient(), instanceID, opts)
+ th.AssertNoErr(t, res.Err)
+}
+
+func TestList(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ HandleList(t)
+
+ expectedDBs := []Database{
+ Database{Name: "anotherexampledb"},
+ Database{Name: "exampledb"},
+ Database{Name: "nextround"},
+ Database{Name: "sampledb"},
+ Database{Name: "testingdb"},
+ }
+
+ pages := 0
+ err := List(fake.ServiceClient(), instanceID).EachPage(func(page pagination.Page) (bool, error) {
+ pages++
+
+ actual, err := ExtractDBs(page)
+ if err != nil {
+ return false, err
+ }
+
+ th.CheckDeepEquals(t, expectedDBs, actual)
+
+ return true, nil
+ })
+
+ th.AssertNoErr(t, err)
+
+ if pages != 1 {
+ t.Errorf("Expected 1 page, saw %d", pages)
+ }
+}
+
+func TestDelete(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ HandleDelete(t)
+
+ err := Delete(fake.ServiceClient(), instanceID, "{dbName}").ExtractErr()
+ th.AssertNoErr(t, err)
+}
diff --git a/openstack/db/v1/databases/results.go b/openstack/db/v1/databases/results.go
new file mode 100644
index 0000000..7d4b6ae
--- /dev/null
+++ b/openstack/db/v1/databases/results.go
@@ -0,0 +1,72 @@
+package databases
+
+import (
+ "github.com/mitchellh/mapstructure"
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+// Database represents a Database API resource.
+type Database struct {
+ // Specifies the name of the MySQL database.
+ Name string
+
+ // Set of symbols and encodings. The default character set is utf8.
+ CharSet string
+
+ // Set of rules for comparing characters in a character set. The default
+ // value for collate is utf8_general_ci.
+ Collate string
+}
+
+// CreateResult represents the result of a Create operation.
+type CreateResult struct {
+ gophercloud.ErrResult
+}
+
+// DeleteResult represents the result of a Delete operation.
+type DeleteResult struct {
+ gophercloud.ErrResult
+}
+
+// DBPage represents a single page of a paginated DB collection.
+type DBPage struct {
+ pagination.LinkedPageBase
+}
+
+// IsEmpty checks to see whether the collection is empty.
+func (page DBPage) IsEmpty() (bool, error) {
+ dbs, err := ExtractDBs(page)
+ if err != nil {
+ return true, err
+ }
+ return len(dbs) == 0, nil
+}
+
+// NextPageURL will retrieve the next page URL.
+func (page DBPage) NextPageURL() (string, error) {
+ type resp struct {
+ Links []gophercloud.Link `mapstructure:"databases_links"`
+ }
+
+ var r resp
+ err := mapstructure.Decode(page.Body, &r)
+ if err != nil {
+ return "", err
+ }
+
+ return gophercloud.ExtractNextURL(r.Links)
+}
+
+// ExtractDBs will convert a generic pagination struct into a more
+// relevant slice of DB structs.
+func ExtractDBs(page pagination.Page) ([]Database, error) {
+ casted := page.(DBPage).Body
+
+ var response struct {
+ Databases []Database `mapstructure:"databases"`
+ }
+
+ err := mapstructure.Decode(casted, &response)
+ return response.Databases, err
+}
diff --git a/openstack/db/v1/databases/urls.go b/openstack/db/v1/databases/urls.go
new file mode 100644
index 0000000..027ca58
--- /dev/null
+++ b/openstack/db/v1/databases/urls.go
@@ -0,0 +1,11 @@
+package databases
+
+import "github.com/rackspace/gophercloud"
+
+func baseURL(c *gophercloud.ServiceClient, instanceID string) string {
+ return c.ServiceURL("instances", instanceID, "databases")
+}
+
+func dbURL(c *gophercloud.ServiceClient, instanceID, dbName string) string {
+ return c.ServiceURL("instances", instanceID, "databases", dbName)
+}
diff --git a/openstack/db/v1/datastores/doc.go b/openstack/db/v1/datastores/doc.go
new file mode 100644
index 0000000..ae14026
--- /dev/null
+++ b/openstack/db/v1/datastores/doc.go
@@ -0,0 +1,3 @@
+// Package datastores provides information and interaction with the datastore
+// API resource in the Rackspace Database service.
+package datastores
diff --git a/openstack/db/v1/datastores/fixtures.go b/openstack/db/v1/datastores/fixtures.go
new file mode 100644
index 0000000..fd767cd
--- /dev/null
+++ b/openstack/db/v1/datastores/fixtures.go
@@ -0,0 +1,100 @@
+package datastores
+
+import (
+ "fmt"
+
+ "github.com/rackspace/gophercloud"
+)
+
+const version1JSON = `
+{
+ "id": "b00000b0-00b0-0b00-00b0-000b000000bb",
+ "links": [
+ {
+ "href": "https://10.240.28.38:8779/v1.0/1234/datastores/versions/b00000b0-00b0-0b00-00b0-000b000000bb",
+ "rel": "self"
+ },
+ {
+ "href": "https://10.240.28.38:8779/datastores/versions/b00000b0-00b0-0b00-00b0-000b000000bb",
+ "rel": "bookmark"
+ }
+ ],
+ "name": "5.1"
+}
+`
+
+const version2JSON = `
+{
+ "id": "c00000b0-00c0-0c00-00c0-000b000000cc",
+ "links": [
+ {
+ "href": "https://10.240.28.38:8779/v1.0/1234/datastores/versions/c00000b0-00c0-0c00-00c0-000b000000cc",
+ "rel": "self"
+ },
+ {
+ "href": "https://10.240.28.38:8779/datastores/versions/c00000b0-00c0-0c00-00c0-000b000000cc",
+ "rel": "bookmark"
+ }
+ ],
+ "name": "5.2"
+}
+`
+
+var versionsJSON = fmt.Sprintf(`"versions": [%s, %s]`, version1JSON, version2JSON)
+
+var singleDSJSON = fmt.Sprintf(`
+{
+ "default_version": "c00000b0-00c0-0c00-00c0-000b000000cc",
+ "id": "10000000-0000-0000-0000-000000000001",
+ "links": [
+ {
+ "href": "https://10.240.28.38:8779/v1.0/1234/datastores/10000000-0000-0000-0000-000000000001",
+ "rel": "self"
+ },
+ {
+ "href": "https://10.240.28.38:8779/datastores/10000000-0000-0000-0000-000000000001",
+ "rel": "bookmark"
+ }
+ ],
+ "name": "mysql",
+ %s
+}
+`, versionsJSON)
+
+var (
+ ListDSResp = fmt.Sprintf(`{"datastores":[%s]}`, singleDSJSON)
+ GetDSResp = fmt.Sprintf(`{"datastore":%s}`, singleDSJSON)
+ ListVersionsResp = fmt.Sprintf(`{%s}`, versionsJSON)
+ GetVersionResp = fmt.Sprintf(`{"version":%s}`, version1JSON)
+)
+
+var ExampleVersion1 = Version{
+ ID: "b00000b0-00b0-0b00-00b0-000b000000bb",
+ Links: []gophercloud.Link{
+ gophercloud.Link{Rel: "self", Href: "https://10.240.28.38:8779/v1.0/1234/datastores/versions/b00000b0-00b0-0b00-00b0-000b000000bb"},
+ gophercloud.Link{Rel: "bookmark", Href: "https://10.240.28.38:8779/datastores/versions/b00000b0-00b0-0b00-00b0-000b000000bb"},
+ },
+ Name: "5.1",
+}
+
+var exampleVersion2 = Version{
+ ID: "c00000b0-00c0-0c00-00c0-000b000000cc",
+ Links: []gophercloud.Link{
+ gophercloud.Link{Rel: "self", Href: "https://10.240.28.38:8779/v1.0/1234/datastores/versions/c00000b0-00c0-0c00-00c0-000b000000cc"},
+ gophercloud.Link{Rel: "bookmark", Href: "https://10.240.28.38:8779/datastores/versions/c00000b0-00c0-0c00-00c0-000b000000cc"},
+ },
+ Name: "5.2",
+}
+
+var ExampleVersions = []Version{ExampleVersion1, exampleVersion2}
+
+var ExampleDatastore = Datastore{
+ DefaultVersion: "c00000b0-00c0-0c00-00c0-000b000000cc",
+ ID: "10000000-0000-0000-0000-000000000001",
+ Links: []gophercloud.Link{
+ gophercloud.Link{Rel: "self", Href: "https://10.240.28.38:8779/v1.0/1234/datastores/10000000-0000-0000-0000-000000000001"},
+ gophercloud.Link{Rel: "bookmark", Href: "https://10.240.28.38:8779/datastores/10000000-0000-0000-0000-000000000001"},
+ },
+ Name: "mysql",
+ Versions: ExampleVersions,
+}
diff --git a/openstack/db/v1/datastores/requests.go b/openstack/db/v1/datastores/requests.go
new file mode 100644
index 0000000..9e147ab
--- /dev/null
+++ b/openstack/db/v1/datastores/requests.go
@@ -0,0 +1,47 @@
+package datastores
+
+import (
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+// List will list all available datastore types that instances can use.
+func List(client *gophercloud.ServiceClient) pagination.Pager {
+ pageFn := func(r pagination.PageResult) pagination.Page {
+ return DatastorePage{pagination.SinglePageBase(r)}
+ }
+ return pagination.NewPager(client, baseURL(client), pageFn)
+}
+
+// Get will retrieve the details of a specified datastore type.
+func Get(client *gophercloud.ServiceClient, datastoreID string) GetResult {
+ var res GetResult
+
+ _, res.Err = client.Request("GET", resourceURL(client, datastoreID), gophercloud.RequestOpts{
+ OkCodes: []int{200},
+ JSONResponse: &res.Body,
+ })
+
+ return res
+}
+
+// ListVersions will list all of the available versions for a specified
+// datastore type.
+func ListVersions(client *gophercloud.ServiceClient, datastoreID string) pagination.Pager {
+ pageFn := func(r pagination.PageResult) pagination.Page {
+ return VersionPage{pagination.SinglePageBase(r)}
+ }
+ return pagination.NewPager(client, versionsURL(client, datastoreID), pageFn)
+}
+
+// GetVersion will retrieve the details of a specified datastore version.
+func GetVersion(client *gophercloud.ServiceClient, datastoreID, versionID string) GetVersionResult {
+ var res GetVersionResult
+
+ _, res.Err = client.Request("GET", versionURL(client, datastoreID, versionID), gophercloud.RequestOpts{
+ OkCodes: []int{200},
+ JSONResponse: &res.Body,
+ })
+
+ return res
+}
diff --git a/openstack/db/v1/datastores/requests_test.go b/openstack/db/v1/datastores/requests_test.go
new file mode 100644
index 0000000..b4ce871
--- /dev/null
+++ b/openstack/db/v1/datastores/requests_test.go
@@ -0,0 +1,78 @@
+package datastores
+
+import (
+ "testing"
+
+ "github.com/rackspace/gophercloud/pagination"
+ th "github.com/rackspace/gophercloud/testhelper"
+ fake "github.com/rackspace/gophercloud/testhelper/client"
+ "github.com/rackspace/gophercloud/testhelper/fixture"
+)
+
+func TestList(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ fixture.SetupHandler(t, "/datastores", "GET", "", ListDSResp, 200)
+
+ pages := 0
+
+ err := List(fake.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) {
+ pages++
+
+ actual, err := ExtractDatastores(page)
+ if err != nil {
+ return false, err
+ }
+
+ th.CheckDeepEquals(t, []Datastore{ExampleDatastore}, actual)
+
+ return true, nil
+ })
+
+ th.AssertNoErr(t, err)
+ th.AssertEquals(t, 1, pages)
+}
+
+func TestGet(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ fixture.SetupHandler(t, "/datastores/{dsID}", "GET", "", GetDSResp, 200)
+
+ ds, err := Get(fake.ServiceClient(), "{dsID}").Extract()
+ th.AssertNoErr(t, err)
+ th.AssertDeepEquals(t, &ExampleDatastore, ds)
+}
+
+func TestListVersions(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ fixture.SetupHandler(t, "/datastores/{dsID}/versions", "GET", "", ListVersionsResp, 200)
+
+ pages := 0
+
+ err := ListVersions(fake.ServiceClient(), "{dsID}").EachPage(func(page pagination.Page) (bool, error) {
+ pages++
+
+ actual, err := ExtractVersions(page)
+ if err != nil {
+ return false, err
+ }
+
+ th.CheckDeepEquals(t, ExampleVersions, actual)
+
+ return true, nil
+ })
+
+ th.AssertNoErr(t, err)
+ th.AssertEquals(t, 1, pages)
+}
+
+func TestGetVersion(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ fixture.SetupHandler(t, "/datastores/{dsID}/versions/{versionID}", "GET", "", GetVersionResp, 200)
+
+ ds, err := GetVersion(fake.ServiceClient(), "{dsID}", "{versionID}").Extract()
+ th.AssertNoErr(t, err)
+ th.AssertDeepEquals(t, &ExampleVersion1, ds)
+}
diff --git a/openstack/db/v1/datastores/results.go b/openstack/db/v1/datastores/results.go
new file mode 100644
index 0000000..a86a3cc
--- /dev/null
+++ b/openstack/db/v1/datastores/results.go
@@ -0,0 +1,123 @@
+package datastores
+
+import (
+ "github.com/mitchellh/mapstructure"
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+// Version represents a version API resource. Multiple versions belong to a Datastore.
+type Version struct {
+ ID string
+ Links []gophercloud.Link
+ Name string
+}
+
+// Datastore represents a Datastore API resource.
+type Datastore struct {
+ DefaultVersion string `json:"default_version" mapstructure:"default_version"`
+ ID string
+ Links []gophercloud.Link
+ Name string
+ Versions []Version
+}
+
+// DatastorePartial is a meta structure which is used in various API responses.
+// It is a lightweight and truncated version of a full Datastore resource,
+// offering details of the Version, Type and VersionID only.
+type DatastorePartial struct {
+ Version string
+ Type string
+ VersionID string `json:"version_id" mapstructure:"version_id"`
+}
+
+// GetResult represents the result of a Get operation.
+type GetResult struct {
+ gophercloud.Result
+}
+
+// GetVersionResult represents the result of getting a version.
+type GetVersionResult struct {
+ gophercloud.Result
+}
+
+// DatastorePage represents a page of datastore resources.
+type DatastorePage struct {
+ pagination.SinglePageBase
+}
+
+// IsEmpty indicates whether a Datastore collection is empty.
+func (r DatastorePage) IsEmpty() (bool, error) {
+ is, err := ExtractDatastores(r)
+ if err != nil {
+ return true, err
+ }
+ return len(is) == 0, nil
+}
+
+// ExtractDatastores retrieves a slice of datastore structs from a paginated
+// collection.
+func ExtractDatastores(page pagination.Page) ([]Datastore, error) {
+ casted := page.(DatastorePage).Body
+
+ var resp struct {
+ Datastores []Datastore `mapstructure:"datastores" json:"datastores"`
+ }
+
+ err := mapstructure.Decode(casted, &resp)
+ return resp.Datastores, err
+}
+
+// Extract retrieves a single Datastore struct from an operation result.
+func (r GetResult) Extract() (*Datastore, error) {
+ if r.Err != nil {
+ return nil, r.Err
+ }
+
+ var response struct {
+ Datastore Datastore `mapstructure:"datastore"`
+ }
+
+ err := mapstructure.Decode(r.Body, &response)
+ return &response.Datastore, err
+}
+
+// DatastorePage represents a page of version resources.
+type VersionPage struct {
+ pagination.SinglePageBase
+}
+
+// IsEmpty indicates whether a collection of version resources is empty.
+func (r VersionPage) IsEmpty() (bool, error) {
+ is, err := ExtractVersions(r)
+ if err != nil {
+ return true, err
+ }
+ return len(is) == 0, nil
+}
+
+// ExtractVersions retrieves a slice of versions from a paginated collection.
+func ExtractVersions(page pagination.Page) ([]Version, error) {
+ casted := page.(VersionPage).Body
+
+ var resp struct {
+ Versions []Version `mapstructure:"versions" json:"versions"`
+ }
+
+ err := mapstructure.Decode(casted, &resp)
+ return resp.Versions, err
+}
+
+// Extract retrieves a single Version struct from an operation result.
+func (r GetVersionResult) Extract() (*Version, error) {
+ if r.Err != nil {
+ return nil, r.Err
+ }
+
+ var response struct {
+ Version Version `mapstructure:"version"`
+ }
+
+ err := mapstructure.Decode(r.Body, &response)
+ return &response.Version, err
+}
diff --git a/openstack/db/v1/datastores/urls.go b/openstack/db/v1/datastores/urls.go
new file mode 100644
index 0000000..c4d5248
--- /dev/null
+++ b/openstack/db/v1/datastores/urls.go
@@ -0,0 +1,19 @@
+package datastores
+
+import "github.com/rackspace/gophercloud"
+
+func baseURL(c *gophercloud.ServiceClient) string {
+ return c.ServiceURL("datastores")
+}
+
+func resourceURL(c *gophercloud.ServiceClient, dsID string) string {
+ return c.ServiceURL("datastores", dsID)
+}
+
+func versionsURL(c *gophercloud.ServiceClient, dsID string) string {
+ return c.ServiceURL("datastores", dsID, "versions")
+}
+
+func versionURL(c *gophercloud.ServiceClient, dsID, versionID string) string {
+ return c.ServiceURL("datastores", dsID, "versions", versionID)
+}
diff --git a/openstack/db/v1/flavors/doc.go b/openstack/db/v1/flavors/doc.go
new file mode 100644
index 0000000..4d281d5
--- /dev/null
+++ b/openstack/db/v1/flavors/doc.go
@@ -0,0 +1,7 @@
+// Package flavors provides information and interaction with the flavor API
+// resource in the OpenStack Database service.
+//
+// A flavor is an available hardware configuration for a database instance.
+// Each flavor has a unique combination of disk space, memory capacity and
+// priority for CPU time.
+package flavors
diff --git a/openstack/db/v1/flavors/fixtures.go b/openstack/db/v1/flavors/fixtures.go
new file mode 100644
index 0000000..f0016bc
--- /dev/null
+++ b/openstack/db/v1/flavors/fixtures.go
@@ -0,0 +1,50 @@
+package flavors
+
+import (
+ "fmt"
+ "testing"
+
+ "github.com/rackspace/gophercloud/testhelper/fixture"
+)
+
+const flavor = `
+{
+ "id": %d,
+ "links": [
+ {
+ "href": "https://openstack.example.com/v1.0/1234/flavors/%d",
+ "rel": "self"
+ },
+ {
+ "href": "https://openstack.example.com/flavors/%d",
+ "rel": "bookmark"
+ }
+ ],
+ "name": "%s",
+ "ram": %d
+}
+`
+
+var (
+ flavorID = "{flavorID}"
+ _baseURL = "/flavors"
+ resURL = "/flavors/" + flavorID
+)
+
+var (
+ flavor1 = fmt.Sprintf(flavor, 1, 1, 1, "m1.tiny", 512)
+ flavor2 = fmt.Sprintf(flavor, 2, 2, 2, "m1.small", 1024)
+ flavor3 = fmt.Sprintf(flavor, 3, 3, 3, "m1.medium", 2048)
+ flavor4 = fmt.Sprintf(flavor, 4, 4, 4, "m1.large", 4096)
+
+ listFlavorsResp = fmt.Sprintf(`{"flavors":[%s, %s, %s, %s]}`, flavor1, flavor2, flavor3, flavor4)
+ getFlavorResp = fmt.Sprintf(`{"flavor": %s}`, flavor1)
+)
+
+func HandleList(t *testing.T) {
+ fixture.SetupHandler(t, _baseURL, "GET", "", listFlavorsResp, 200)
+}
+
+func HandleGet(t *testing.T) {
+ fixture.SetupHandler(t, resURL, "GET", "", getFlavorResp, 200)
+}
diff --git a/openstack/db/v1/flavors/requests.go b/openstack/db/v1/flavors/requests.go
new file mode 100644
index 0000000..fa34446
--- /dev/null
+++ b/openstack/db/v1/flavors/requests.go
@@ -0,0 +1,29 @@
+package flavors
+
+import (
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+// List will list all available hardware flavors that an instance can use. The
+// operation is identical to the one supported by the Nova API, but without the
+// "disk" property.
+func List(client *gophercloud.ServiceClient) pagination.Pager {
+ createPage := func(r pagination.PageResult) pagination.Page {
+ return FlavorPage{pagination.LinkedPageBase{PageResult: r}}
+ }
+
+ return pagination.NewPager(client, listURL(client), createPage)
+}
+
+// Get will retrieve information for a specified hardware flavor.
+func Get(client *gophercloud.ServiceClient, id string) GetResult {
+ var gr GetResult
+
+ _, gr.Err = client.Request("GET", getURL(client, id), gophercloud.RequestOpts{
+ JSONResponse: &gr.Body,
+ OkCodes: []int{200},
+ })
+
+ return gr
+}
diff --git a/openstack/db/v1/flavors/requests_test.go b/openstack/db/v1/flavors/requests_test.go
new file mode 100644
index 0000000..88b5871
--- /dev/null
+++ b/openstack/db/v1/flavors/requests_test.go
@@ -0,0 +1,91 @@
+package flavors
+
+import (
+ "testing"
+
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+ th "github.com/rackspace/gophercloud/testhelper"
+ fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestListFlavors(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ HandleList(t)
+
+ pages := 0
+ err := List(fake.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) {
+ pages++
+
+ actual, err := ExtractFlavors(page)
+ if err != nil {
+ return false, err
+ }
+
+ expected := []Flavor{
+ Flavor{
+ ID: "1",
+ Name: "m1.tiny",
+ RAM: 512,
+ Links: []gophercloud.Link{
+ gophercloud.Link{Href: "https://openstack.example.com/v1.0/1234/flavors/1", Rel: "self"},
+ gophercloud.Link{Href: "https://openstack.example.com/flavors/1", Rel: "bookmark"},
+ },
+ },
+ Flavor{
+ ID: "2",
+ Name: "m1.small",
+ RAM: 1024,
+ Links: []gophercloud.Link{
+ gophercloud.Link{Href: "https://openstack.example.com/v1.0/1234/flavors/2", Rel: "self"},
+ gophercloud.Link{Href: "https://openstack.example.com/flavors/2", Rel: "bookmark"},
+ },
+ },
+ Flavor{
+ ID: "3",
+ Name: "m1.medium",
+ RAM: 2048,
+ Links: []gophercloud.Link{
+ gophercloud.Link{Href: "https://openstack.example.com/v1.0/1234/flavors/3", Rel: "self"},
+ gophercloud.Link{Href: "https://openstack.example.com/flavors/3", Rel: "bookmark"},
+ },
+ },
+ Flavor{
+ ID: "4",
+ Name: "m1.large",
+ RAM: 4096,
+ Links: []gophercloud.Link{
+ gophercloud.Link{Href: "https://openstack.example.com/v1.0/1234/flavors/4", Rel: "self"},
+ gophercloud.Link{Href: "https://openstack.example.com/flavors/4", Rel: "bookmark"},
+ },
+ },
+ }
+
+ th.AssertDeepEquals(t, expected, actual)
+ return true, nil
+ })
+
+ th.AssertNoErr(t, err)
+ th.AssertEquals(t, 1, pages)
+}
+
+func TestGetFlavor(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ HandleGet(t)
+
+ actual, err := Get(fake.ServiceClient(), flavorID).Extract()
+ th.AssertNoErr(t, err)
+
+ expected := &Flavor{
+ ID: "1",
+ Name: "m1.tiny",
+ RAM: 512,
+ Links: []gophercloud.Link{
+ gophercloud.Link{Href: "https://openstack.example.com/v1.0/1234/flavors/1", Rel: "self"},
+ },
+ }
+
+ th.AssertDeepEquals(t, expected, actual)
+}
diff --git a/openstack/db/v1/flavors/results.go b/openstack/db/v1/flavors/results.go
new file mode 100644
index 0000000..2cee010
--- /dev/null
+++ b/openstack/db/v1/flavors/results.go
@@ -0,0 +1,92 @@
+package flavors
+
+import (
+ "github.com/mitchellh/mapstructure"
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+// GetResult temporarily holds the response from a Get call.
+type GetResult struct {
+ gophercloud.Result
+}
+
+// Extract provides access to the individual Flavor returned by the Get function.
+func (gr GetResult) Extract() (*Flavor, error) {
+ if gr.Err != nil {
+ return nil, gr.Err
+ }
+
+ var result struct {
+ Flavor Flavor `mapstructure:"flavor"`
+ }
+
+ decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
+ WeaklyTypedInput: true,
+ Result: &result,
+ })
+
+ err = decoder.Decode(gr.Body)
+ return &result.Flavor, err
+}
+
+// Flavor records represent (virtual) hardware configurations for server resources in a region.
+type Flavor struct {
+ // The flavor's unique identifier.
+ ID string `mapstructure:"id"`
+
+ // The RAM capacity for the flavor.
+ RAM int `mapstructure:"ram"`
+
+ // The Name field provides a human-readable moniker for the flavor.
+ Name string `mapstructure:"name"`
+
+ // Links to access the flavor.
+ Links []gophercloud.Link
+}
+
+// FlavorPage contains a single page of the response from a List call.
+type FlavorPage struct {
+ pagination.LinkedPageBase
+}
+
+// IsEmpty determines if a page contains any results.
+func (p FlavorPage) IsEmpty() (bool, error) {
+ flavors, err := ExtractFlavors(p)
+ if err != nil {
+ return true, err
+ }
+ return len(flavors) == 0, nil
+}
+
+// NextPageURL uses the response's embedded link reference to navigate to the next page of results.
+func (p FlavorPage) NextPageURL() (string, error) {
+ type resp struct {
+ Links []gophercloud.Link `mapstructure:"flavors_links"`
+ }
+
+ var r resp
+ err := mapstructure.Decode(p.Body, &r)
+ if err != nil {
+ return "", err
+ }
+
+ return gophercloud.ExtractNextURL(r.Links)
+}
+
+// ExtractFlavors provides access to the list of flavors in a page acquired from the List operation.
+func ExtractFlavors(page pagination.Page) ([]Flavor, error) {
+ casted := page.(FlavorPage).Body
+ var container struct {
+ Flavors []Flavor `mapstructure:"flavors"`
+ }
+
+ decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
+ WeaklyTypedInput: true,
+ Result: &container,
+ })
+
+ err = decoder.Decode(casted)
+
+ return container.Flavors, err
+}
diff --git a/openstack/db/v1/flavors/urls.go b/openstack/db/v1/flavors/urls.go
new file mode 100644
index 0000000..80da11f
--- /dev/null
+++ b/openstack/db/v1/flavors/urls.go
@@ -0,0 +1,11 @@
+package flavors
+
+import "github.com/rackspace/gophercloud"
+
+func getURL(client *gophercloud.ServiceClient, id string) string {
+ return client.ServiceURL("flavors", id)
+}
+
+func listURL(client *gophercloud.ServiceClient) string {
+ return client.ServiceURL("flavors")
+}
diff --git a/openstack/db/v1/instances/doc.go b/openstack/db/v1/instances/doc.go
new file mode 100644
index 0000000..dc5c90f
--- /dev/null
+++ b/openstack/db/v1/instances/doc.go
@@ -0,0 +1,7 @@
+// Package instances provides information and interaction with the instance API
+// resource in the OpenStack Database service.
+//
+// A database instance is an isolated database environment with compute and
+// storage resources in a single tenant environment on a shared physical host
+// machine.
+package instances
diff --git a/openstack/db/v1/instances/fixtures.go b/openstack/db/v1/instances/fixtures.go
new file mode 100644
index 0000000..af7b185
--- /dev/null
+++ b/openstack/db/v1/instances/fixtures.go
@@ -0,0 +1,169 @@
+package instances
+
+import (
+ "fmt"
+ "testing"
+ "time"
+
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/openstack/db/v1/datastores"
+ "github.com/rackspace/gophercloud/openstack/db/v1/flavors"
+ "github.com/rackspace/gophercloud/testhelper/fixture"
+)
+
+var (
+ timestamp = "2015-11-12T14:22:42Z"
+ timeVal, _ = time.Parse(time.RFC3339, timestamp)
+)
+
+var instance = `
+{
+ "created": "` + timestamp + `",
+ "datastore": {
+ "type": "mysql",
+ "version": "5.6"
+ },
+ "flavor": {
+ "id": "1",
+ "links": [
+ {
+ "href": "https://my-openstack.com/v1.0/1234/flavors/1",
+ "rel": "self"
+ },
+ {
+ "href": "https://my-openstack.com/v1.0/1234/flavors/1",
+ "rel": "bookmark"
+ }
+ ]
+ },
+ "links": [
+ {
+ "href": "https://my-openstack.com/v1.0/1234/instances/1",
+ "rel": "self"
+ }
+ ],
+ "hostname": "e09ad9a3f73309469cf1f43d11e79549caf9acf2.my-openstack.com",
+ "id": "{instanceID}",
+ "name": "json_rack_instance",
+ "status": "BUILD",
+ "updated": "` + timestamp + `",
+ "volume": {
+ "size": 2
+ }
+}
+`
+
+var createReq = `
+{
+ "instance": {
+ "databases": [
+ {
+ "character_set": "utf8",
+ "collate": "utf8_general_ci",
+ "name": "sampledb"
+ },
+ {
+ "name": "nextround"
+ }
+ ],
+ "flavorRef": "1",
+ "name": "json_rack_instance",
+ "users": [
+ {
+ "databases": [
+ {
+ "name": "sampledb"
+ }
+ ],
+ "name": "demouser",
+ "password": "demopassword"
+ }
+ ],
+ "volume": {
+ "size": 2
+ }
+ }
+}
+`
+
+var (
+ instanceID = "{instanceID}"
+ rootURL = "/instances"
+ resURL = rootURL + "/" + instanceID
+ uRootURL = resURL + "/root"
+ aURL = resURL + "/action"
+)
+
+var (
+ restartReq = `{"restart": {}}`
+ resizeReq = `{"resize": {"flavorRef": "2"}}`
+ resizeVolReq = `{"resize": {"volume": {"size": 4}}}`
+)
+
+var (
+ createResp = fmt.Sprintf(`{"instance": %s}`, instance)
+ listInstancesResp = fmt.Sprintf(`{"instances":[%s]}`, instance)
+ getInstanceResp = createResp
+ enableUserResp = `{"user":{"name":"root","password":"secretsecret"}}`
+ isUserEnabledResp = `{"rootEnabled":true}`
+)
+
+var expectedInstance = Instance{
+ Created: timeVal,
+ Updated: timeVal,
+ Flavor: flavors.Flavor{
+ ID: "1",
+ Links: []gophercloud.Link{
+ gophercloud.Link{Href: "https://my-openstack.com/v1.0/1234/flavors/1", Rel: "self"},
+ gophercloud.Link{Href: "https://my-openstack.com/v1.0/1234/flavors/1", Rel: "bookmark"},
+ },
+ },
+ Hostname: "e09ad9a3f73309469cf1f43d11e79549caf9acf2.my-openstack.com",
+ ID: instanceID,
+ Links: []gophercloud.Link{
+ gophercloud.Link{Href: "https://my-openstack.com/v1.0/1234/instances/1", Rel: "self"},
+ },
+ Name: "json_rack_instance",
+ Status: "BUILD",
+ Volume: Volume{Size: 2},
+ Datastore: datastores.DatastorePartial{
+ Type: "mysql",
+ Version: "5.6",
+ },
+}
+
+func HandleCreate(t *testing.T) {
+ fixture.SetupHandler(t, rootURL, "POST", createReq, createResp, 200)
+}
+
+func HandleList(t *testing.T) {
+ fixture.SetupHandler(t, rootURL, "GET", "", listInstancesResp, 200)
+}
+
+func HandleGet(t *testing.T) {
+ fixture.SetupHandler(t, resURL, "GET", "", getInstanceResp, 200)
+}
+
+func HandleDelete(t *testing.T) {
+ fixture.SetupHandler(t, resURL, "DELETE", "", "", 202)
+}
+
+func HandleEnableRoot(t *testing.T) {
+ fixture.SetupHandler(t, uRootURL, "POST", "", enableUserResp, 200)
+}
+
+func HandleIsRootEnabled(t *testing.T) {
+ fixture.SetupHandler(t, uRootURL, "GET", "", isUserEnabledResp, 200)
+}
+
+func HandleRestart(t *testing.T) {
+ fixture.SetupHandler(t, aURL, "POST", restartReq, "", 202)
+}
+
+func HandleResize(t *testing.T) {
+ fixture.SetupHandler(t, aURL, "POST", resizeReq, "", 202)
+}
+
+func HandleResizeVol(t *testing.T) {
+ fixture.SetupHandler(t, aURL, "POST", resizeVolReq, "", 202)
+}
diff --git a/openstack/db/v1/instances/requests.go b/openstack/db/v1/instances/requests.go
new file mode 100644
index 0000000..f4a63b8
--- /dev/null
+++ b/openstack/db/v1/instances/requests.go
@@ -0,0 +1,238 @@
+package instances
+
+import (
+ "fmt"
+
+ "github.com/rackspace/gophercloud"
+ db "github.com/rackspace/gophercloud/openstack/db/v1/databases"
+ "github.com/rackspace/gophercloud/openstack/db/v1/users"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+// CreateOptsBuilder is the top-level interface for create options.
+type CreateOptsBuilder interface {
+ ToInstanceCreateMap() (map[string]interface{}, error)
+}
+
+// DatastoreOpts represents the configuration for how an instance stores data.
+type DatastoreOpts struct {
+ Version string
+ Type string
+}
+
+func (opts DatastoreOpts) ToMap() (map[string]string, error) {
+ return map[string]string{
+ "version": opts.Version,
+ "type": opts.Type,
+ }, nil
+}
+
+// CreateOpts is the struct responsible for configuring a new database instance.
+type CreateOpts struct {
+ // Either the integer UUID (in string form) of the flavor, or its URI
+ // reference as specified in the response from the List() call. Required.
+ FlavorRef string
+
+ // Specifies the volume size in gigabytes (GB). The value must be between 1
+ // and 300. Required.
+ Size int
+
+ // Name of the instance to create. The length of the name is limited to
+ // 255 characters and any characters are permitted. Optional.
+ Name string
+
+ // A slice of database information options.
+ Databases db.CreateOptsBuilder
+
+ // A slice of user information options.
+ Users users.CreateOptsBuilder
+
+ // Options to configure the type of datastore the instance will use. This is
+ // optional, and if excluded will default to MySQL.
+ Datastore *DatastoreOpts
+}
+
+// ToInstanceCreateMap will render a JSON map.
+func (opts CreateOpts) ToInstanceCreateMap() (map[string]interface{}, error) {
+ if opts.Size > 300 || opts.Size < 1 {
+ return nil, fmt.Errorf("Size (GB) must be between 1-300")
+ }
+ if opts.FlavorRef == "" {
+ return nil, fmt.Errorf("FlavorRef is a required field")
+ }
+
+ instance := map[string]interface{}{
+ "volume": map[string]int{"size": opts.Size},
+ "flavorRef": opts.FlavorRef,
+ }
+
+ if opts.Name != "" {
+ instance["name"] = opts.Name
+ }
+ if opts.Databases != nil {
+ dbs, err := opts.Databases.ToDBCreateMap()
+ if err != nil {
+ return nil, err
+ }
+ instance["databases"] = dbs["databases"]
+ }
+ if opts.Users != nil {
+ users, err := opts.Users.ToUserCreateMap()
+ if err != nil {
+ return nil, err
+ }
+ instance["users"] = users["users"]
+ }
+
+ return map[string]interface{}{"instance": instance}, nil
+}
+
+// Create asynchronously provisions a new database instance. It requires the
+// user to specify a flavor and a volume size. The API service then provisions
+// the instance with the requested flavor and sets up a volume of the specified
+// size, which is the storage for the database instance.
+//
+// Although this call only allows the creation of 1 instance per request, you
+// can create an instance with multiple databases and users. The default
+// binding for a MySQL instance is port 3306.
+func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) CreateResult {
+ var res CreateResult
+
+ reqBody, err := opts.ToInstanceCreateMap()
+ if err != nil {
+ res.Err = err
+ return res
+ }
+
+ _, res.Err = client.Request("POST", baseURL(client), gophercloud.RequestOpts{
+ JSONBody: &reqBody,
+ JSONResponse: &res.Body,
+ OkCodes: []int{200},
+ })
+
+ return res
+}
+
+// List retrieves the status and information for all database instances.
+func List(client *gophercloud.ServiceClient) pagination.Pager {
+ createPageFn := func(r pagination.PageResult) pagination.Page {
+ return InstancePage{pagination.LinkedPageBase{PageResult: r}}
+ }
+
+ return pagination.NewPager(client, baseURL(client), createPageFn)
+}
+
+// Get retrieves the status and information for a specified database instance.
+func Get(client *gophercloud.ServiceClient, id string) GetResult {
+ var res GetResult
+
+ _, res.Err = client.Request("GET", resourceURL(client, id), gophercloud.RequestOpts{
+ JSONResponse: &res.Body,
+ OkCodes: []int{200},
+ })
+
+ return res
+}
+
+// Delete permanently destroys the database instance.
+func Delete(client *gophercloud.ServiceClient, id string) DeleteResult {
+ var res DeleteResult
+
+ _, res.Err = client.Request("DELETE", resourceURL(client, id), gophercloud.RequestOpts{
+ OkCodes: []int{202},
+ })
+
+ return res
+}
+
+// EnableRootUser enables the login from any host for the root user and
+// provides the user with a generated root password.
+func EnableRootUser(client *gophercloud.ServiceClient, id string) UserRootResult {
+ var res UserRootResult
+
+ _, res.Err = client.Request("POST", userRootURL(client, id), gophercloud.RequestOpts{
+ JSONResponse: &res.Body,
+ OkCodes: []int{200},
+ })
+
+ return res
+}
+
+// IsRootEnabled checks an instance to see if root access is enabled. It returns
+// True if root user is enabled for the specified database instance or False
+// otherwise.
+func IsRootEnabled(client *gophercloud.ServiceClient, id string) (bool, error) {
+ var res gophercloud.Result
+
+ _, err := client.Request("GET", userRootURL(client, id), gophercloud.RequestOpts{
+ JSONResponse: &res.Body,
+ OkCodes: []int{200},
+ })
+
+ return res.Body.(map[string]interface{})["rootEnabled"] == true, err
+}
+
+// Restart will restart only the MySQL Instance. Restarting MySQL will
+// erase any dynamic configuration settings that you have made within MySQL.
+// The MySQL service will be unavailable until the instance restarts.
+func Restart(client *gophercloud.ServiceClient, id string) ActionResult {
+ var res ActionResult
+
+ _, res.Err = client.Request("POST", actionURL(client, id), gophercloud.RequestOpts{
+ JSONBody: map[string]interface{}{"restart": struct{}{}},
+ OkCodes: []int{202},
+ })
+
+ return res
+}
+
+// Resize changes the memory size of the instance, assuming a valid
+// flavorRef is provided. It will also restart the MySQL service.
+func Resize(client *gophercloud.ServiceClient, id, flavorRef string) ActionResult {
+ var res ActionResult
+
+ type resize struct {
+ FlavorRef string `json:"flavorRef"`
+ }
+
+ type req struct {
+ Resize resize `json:"resize"`
+ }
+
+ reqBody := req{Resize: resize{FlavorRef: flavorRef}}
+
+ _, res.Err = client.Request("POST", actionURL(client, id), gophercloud.RequestOpts{
+ JSONBody: reqBody,
+ OkCodes: []int{202},
+ })
+
+ return res
+}
+
+// ResizeVolume will resize the attached volume for an instance. It supports
+// only increasing the volume size and does not support decreasing the size.
+// The volume size is in gigabytes (GB) and must be an integer.
+func ResizeVolume(client *gophercloud.ServiceClient, id string, size int) ActionResult {
+ var res ActionResult
+
+ type volume struct {
+ Size int `json:"size"`
+ }
+
+ type resize struct {
+ Volume volume `json:"volume"`
+ }
+
+ type req struct {
+ Resize resize `json:"resize"`
+ }
+
+ reqBody := req{Resize: resize{Volume: volume{Size: size}}}
+
+ _, res.Err = client.Request("POST", actionURL(client, id), gophercloud.RequestOpts{
+ JSONBody: reqBody,
+ OkCodes: []int{202},
+ })
+
+ return res
+}
diff --git a/openstack/db/v1/instances/requests_test.go b/openstack/db/v1/instances/requests_test.go
new file mode 100644
index 0000000..3cc2b70
--- /dev/null
+++ b/openstack/db/v1/instances/requests_test.go
@@ -0,0 +1,133 @@
+package instances
+
+import (
+ "testing"
+
+ db "github.com/rackspace/gophercloud/openstack/db/v1/databases"
+ "github.com/rackspace/gophercloud/openstack/db/v1/users"
+ "github.com/rackspace/gophercloud/pagination"
+ th "github.com/rackspace/gophercloud/testhelper"
+ fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestCreate(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ HandleCreate(t)
+
+ opts := CreateOpts{
+ Name: "json_rack_instance",
+ FlavorRef: "1",
+ Databases: db.BatchCreateOpts{
+ db.CreateOpts{CharSet: "utf8", Collate: "utf8_general_ci", Name: "sampledb"},
+ db.CreateOpts{Name: "nextround"},
+ },
+ Users: users.BatchCreateOpts{
+ users.CreateOpts{
+ Name: "demouser",
+ Password: "demopassword",
+ Databases: db.BatchCreateOpts{
+ db.CreateOpts{Name: "sampledb"},
+ },
+ },
+ },
+ Size: 2,
+ }
+
+ instance, err := Create(fake.ServiceClient(), opts).Extract()
+
+ th.AssertNoErr(t, err)
+ th.AssertDeepEquals(t, &expectedInstance, instance)
+}
+
+func TestInstanceList(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ HandleList(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
+ }
+
+ th.CheckDeepEquals(t, []Instance{expectedInstance}, actual)
+ return true, nil
+ })
+
+ th.AssertNoErr(t, err)
+ th.AssertEquals(t, 1, pages)
+}
+
+func TestGetInstance(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ HandleGet(t)
+
+ instance, err := Get(fake.ServiceClient(), instanceID).Extract()
+
+ th.AssertNoErr(t, err)
+ th.AssertDeepEquals(t, &expectedInstance, instance)
+}
+
+func TestDeleteInstance(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ HandleDelete(t)
+
+ res := Delete(fake.ServiceClient(), instanceID)
+ th.AssertNoErr(t, res.Err)
+}
+
+func TestEnableRootUser(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ HandleEnableRoot(t)
+
+ expected := &users.User{Name: "root", Password: "secretsecret"}
+ user, err := EnableRootUser(fake.ServiceClient(), instanceID).Extract()
+
+ th.AssertNoErr(t, err)
+ th.AssertDeepEquals(t, expected, user)
+}
+
+func TestIsRootEnabled(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ HandleIsRootEnabled(t)
+
+ isEnabled, err := IsRootEnabled(fake.ServiceClient(), instanceID)
+
+ th.AssertNoErr(t, err)
+ th.AssertEquals(t, true, isEnabled)
+}
+
+func TestRestart(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ HandleRestart(t)
+
+ res := Restart(fake.ServiceClient(), instanceID)
+ th.AssertNoErr(t, res.Err)
+}
+
+func TestResize(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ HandleResize(t)
+
+ res := Resize(fake.ServiceClient(), instanceID, "2")
+ th.AssertNoErr(t, res.Err)
+}
+
+func TestResizeVolume(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ HandleResizeVol(t)
+
+ res := ResizeVolume(fake.ServiceClient(), instanceID, 4)
+ th.AssertNoErr(t, res.Err)
+}
diff --git a/openstack/db/v1/instances/results.go b/openstack/db/v1/instances/results.go
new file mode 100644
index 0000000..9a49510
--- /dev/null
+++ b/openstack/db/v1/instances/results.go
@@ -0,0 +1,205 @@
+package instances
+
+import (
+ "time"
+
+ "github.com/mitchellh/mapstructure"
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/openstack/db/v1/datastores"
+ "github.com/rackspace/gophercloud/openstack/db/v1/flavors"
+ "github.com/rackspace/gophercloud/openstack/db/v1/users"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+// Volume represents information about an attached volume for a database instance.
+type Volume struct {
+ // The size in GB of the volume
+ Size int
+
+ Used float64
+}
+
+// Instance represents a remote MySQL instance.
+type Instance struct {
+ // Indicates the datetime that the instance was created
+ Created time.Time `mapstructure:"-"`
+
+ // Indicates the most recent datetime that the instance was updated.
+ Updated time.Time `mapstructure:"-"`
+
+ // Indicates the hardware flavor the instance uses.
+ Flavor flavors.Flavor
+
+ // A DNS-resolvable hostname associated with the database instance (rather
+ // than an IPv4 address). Since the hostname always resolves to the correct
+ // IP address of the database instance, this relieves the user from the task
+ // of maintaining the mapping. Note that although the IP address may likely
+ // change on resizing, migrating, and so forth, the hostname always resolves
+ // to the correct database instance.
+ Hostname string
+
+ // Indicates the unique identifier for the instance resource.
+ ID string
+
+ // Exposes various links that reference the instance resource.
+ Links []gophercloud.Link
+
+ // The human-readable name of the instance.
+ Name string
+
+ // The build status of the instance.
+ Status string
+
+ // Information about the attached volume of the instance.
+ Volume Volume
+
+ // Indicates how the instance stores data.
+ Datastore datastores.DatastorePartial
+}
+
+type commonResult struct {
+ gophercloud.Result
+}
+
+// CreateResult represents the result of a Create operation.
+type CreateResult struct {
+ commonResult
+}
+
+// GetResult represents the result of a Get operation.
+type GetResult struct {
+ commonResult
+}
+
+// DeleteResult represents the result of a Delete operation.
+type DeleteResult struct {
+ gophercloud.ErrResult
+}
+
+// Extract will extract an Instance from various result structs.
+func (r commonResult) Extract() (*Instance, error) {
+ if r.Err != nil {
+ return nil, r.Err
+ }
+
+ var response struct {
+ Instance Instance `mapstructure:"instance"`
+ }
+
+ err := mapstructure.Decode(r.Body, &response)
+ val := r.Body.(map[string]interface{})["instance"].(map[string]interface{})
+
+ if t, ok := val["created"].(string); ok && t != "" {
+ creationTime, err := time.Parse(time.RFC3339, t)
+ if err != nil {
+ return &response.Instance, err
+ }
+ response.Instance.Created = creationTime
+ }
+
+ if t, ok := val["updated"].(string); ok && t != "" {
+ updatedTime, err := time.Parse(time.RFC3339, t)
+ if err != nil {
+ return &response.Instance, err
+ }
+ response.Instance.Updated = updatedTime
+ }
+
+ return &response.Instance, err
+}
+
+// InstancePage represents a single page of a paginated instance collection.
+type InstancePage struct {
+ pagination.LinkedPageBase
+}
+
+// IsEmpty checks to see whether the collection is empty.
+func (page InstancePage) IsEmpty() (bool, error) {
+ instances, err := ExtractInstances(page)
+ if err != nil {
+ return true, err
+ }
+ return len(instances) == 0, nil
+}
+
+// NextPageURL will retrieve the next page URL.
+func (page InstancePage) NextPageURL() (string, error) {
+ type resp struct {
+ Links []gophercloud.Link `mapstructure:"instances_links"`
+ }
+
+ var r resp
+ err := mapstructure.Decode(page.Body, &r)
+ if err != nil {
+ return "", err
+ }
+
+ return gophercloud.ExtractNextURL(r.Links)
+}
+
+// ExtractInstances will convert a generic pagination struct into a more
+// relevant slice of Instance structs.
+func ExtractInstances(page pagination.Page) ([]Instance, error) {
+ casted := page.(InstancePage).Body
+
+ var response struct {
+ Instances []Instance `mapstructure:"instances"`
+ }
+
+ err := mapstructure.Decode(casted, &response)
+
+ var vals []interface{}
+ switch (casted).(type) {
+ case interface{}:
+ vals = casted.(map[string]interface{})["instances"].([]interface{})
+ }
+
+ for i, v := range vals {
+ val := v.(map[string]interface{})
+
+ if t, ok := val["created"].(string); ok && t != "" {
+ creationTime, err := time.Parse(time.RFC3339, t)
+ if err != nil {
+ return response.Instances, err
+ }
+ response.Instances[i].Created = creationTime
+ }
+
+ if t, ok := val["updated"].(string); ok && t != "" {
+ updatedTime, err := time.Parse(time.RFC3339, t)
+ if err != nil {
+ return response.Instances, err
+ }
+ response.Instances[i].Updated = updatedTime
+ }
+ }
+
+ return response.Instances, err
+}
+
+// UserRootResult represents the result of an operation to enable the root user.
+type UserRootResult struct {
+ gophercloud.Result
+}
+
+// Extract will extract root user information from a UserRootResult.
+func (r UserRootResult) Extract() (*users.User, error) {
+ if r.Err != nil {
+ return nil, r.Err
+ }
+
+ var response struct {
+ User users.User `mapstructure:"user"`
+ }
+
+ err := mapstructure.Decode(r.Body, &response)
+
+ return &response.User, err
+}
+
+// ActionResult represents the result of action requests, such as: restarting
+// an instance service, resizing its memory allocation, and resizing its
+// attached volume size.
+type ActionResult struct {
+ gophercloud.ErrResult
+}
diff --git a/openstack/db/v1/instances/urls.go b/openstack/db/v1/instances/urls.go
new file mode 100644
index 0000000..28c0bec
--- /dev/null
+++ b/openstack/db/v1/instances/urls.go
@@ -0,0 +1,19 @@
+package instances
+
+import "github.com/rackspace/gophercloud"
+
+func baseURL(c *gophercloud.ServiceClient) string {
+ return c.ServiceURL("instances")
+}
+
+func resourceURL(c *gophercloud.ServiceClient, id string) string {
+ return c.ServiceURL("instances", id)
+}
+
+func userRootURL(c *gophercloud.ServiceClient, id string) string {
+ return c.ServiceURL("instances", id, "root")
+}
+
+func actionURL(c *gophercloud.ServiceClient, id string) string {
+ return c.ServiceURL("instances", id, "action")
+}
diff --git a/openstack/db/v1/users/doc.go b/openstack/db/v1/users/doc.go
new file mode 100644
index 0000000..cf07832
--- /dev/null
+++ b/openstack/db/v1/users/doc.go
@@ -0,0 +1,3 @@
+// Package users provides information and interaction with the user API
+// resource in the OpenStack Database service.
+package users
diff --git a/openstack/db/v1/users/fixtures.go b/openstack/db/v1/users/fixtures.go
new file mode 100644
index 0000000..516b335
--- /dev/null
+++ b/openstack/db/v1/users/fixtures.go
@@ -0,0 +1,37 @@
+package users
+
+import (
+ "fmt"
+ "testing"
+
+ "github.com/rackspace/gophercloud/testhelper/fixture"
+)
+
+const user1 = `
+{"databases": [{"name": "databaseA"}],"name": "dbuser3"%s}
+`
+
+const user2 = `
+{"databases": [{"name": "databaseB"},{"name": "databaseC"}],"name": "dbuser4"%s}
+`
+
+var (
+ instanceID = "{instanceID}"
+ _rootURL = "/instances/" + instanceID + "/users"
+ pUser1 = fmt.Sprintf(user1, `,"password":"secretsecret"`)
+ pUser2 = fmt.Sprintf(user2, `,"password":"secretsecret"`)
+ createReq = fmt.Sprintf(`{"users":[%s, %s]}`, pUser1, pUser2)
+ listResp = fmt.Sprintf(`{"users":[%s, %s]}`, fmt.Sprintf(user1, ""), fmt.Sprintf(user2, ""))
+)
+
+func HandleCreate(t *testing.T) {
+ fixture.SetupHandler(t, _rootURL, "POST", createReq, "", 202)
+}
+
+func HandleList(t *testing.T) {
+ fixture.SetupHandler(t, _rootURL, "GET", "", listResp, 200)
+}
+
+func HandleDelete(t *testing.T) {
+ fixture.SetupHandler(t, _rootURL+"/{userName}", "DELETE", "", "", 202)
+}
diff --git a/openstack/db/v1/users/requests.go b/openstack/db/v1/users/requests.go
new file mode 100644
index 0000000..7533fc4
--- /dev/null
+++ b/openstack/db/v1/users/requests.go
@@ -0,0 +1,132 @@
+package users
+
+import (
+ "errors"
+
+ "github.com/rackspace/gophercloud"
+ db "github.com/rackspace/gophercloud/openstack/db/v1/databases"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+// CreateOptsBuilder is the top-level interface for creating JSON maps.
+type CreateOptsBuilder interface {
+ ToUserCreateMap() (map[string]interface{}, error)
+}
+
+// CreateOpts is the struct responsible for configuring a new user; often in the
+// context of an instance.
+type CreateOpts struct {
+ // [REQUIRED] Specifies a name for the user. Valid names can be composed
+ // of the following characters: letters (either case); numbers; these
+ // characters '@', '?', '#', ' ' but NEVER beginning a name string; '_' is
+ // permitted anywhere. Prohibited characters that are forbidden include:
+ // single quotes, double quotes, back quotes, semicolons, commas, backslashes,
+ // and forward slashes. Spaces at the front or end of a user name are also
+ // not permitted.
+ Name string
+
+ // [REQUIRED] Specifies a password for the user.
+ Password string
+
+ // [OPTIONAL] An array of databases that this user will connect to. The
+ // "name" field is the only requirement for each option.
+ Databases db.BatchCreateOpts
+
+ // [OPTIONAL] Specifies the host from which a user is allowed to connect to
+ // the database. Possible values are a string containing an IPv4 address or
+ // "%" to allow connecting from any host. Optional; the default is "%".
+ Host string
+}
+
+// ToMap is a convenience function for creating sub-maps for individual users.
+func (opts CreateOpts) ToMap() (map[string]interface{}, error) {
+
+ if opts.Name == "root" {
+ return nil, errors.New("root is a reserved user name and cannot be used")
+ }
+ if opts.Name == "" {
+ return nil, errors.New("Name is a required field")
+ }
+ if opts.Password == "" {
+ return nil, errors.New("Password is a required field")
+ }
+
+ user := map[string]interface{}{
+ "name": opts.Name,
+ "password": opts.Password,
+ }
+
+ if opts.Host != "" {
+ user["host"] = opts.Host
+ }
+
+ dbs := make([]map[string]string, len(opts.Databases))
+ for i, db := range opts.Databases {
+ dbs[i] = map[string]string{"name": db.Name}
+ }
+
+ if len(dbs) > 0 {
+ user["databases"] = dbs
+ }
+
+ return user, nil
+}
+
+// BatchCreateOpts allows multiple users to be created at once.
+type BatchCreateOpts []CreateOpts
+
+// ToUserCreateMap will generate a JSON map.
+func (opts BatchCreateOpts) ToUserCreateMap() (map[string]interface{}, error) {
+ users := make([]map[string]interface{}, len(opts))
+ for i, opt := range opts {
+ user, err := opt.ToMap()
+ if err != nil {
+ return nil, err
+ }
+ users[i] = user
+ }
+ return map[string]interface{}{"users": users}, nil
+}
+
+// Create asynchronously provisions a new user for the specified database
+// instance based on the configuration defined in CreateOpts. If databases are
+// assigned for a particular user, the user will be granted all privileges
+// for those specified databases. "root" is a reserved name and cannot be used.
+func Create(client *gophercloud.ServiceClient, instanceID string, opts CreateOptsBuilder) CreateResult {
+ var res CreateResult
+
+ reqBody, err := opts.ToUserCreateMap()
+ if err != nil {
+ res.Err = err
+ return res
+ }
+
+ _, res.Err = client.Request("POST", baseURL(client, instanceID), gophercloud.RequestOpts{
+ JSONBody: &reqBody,
+ OkCodes: []int{202},
+ })
+
+ return res
+}
+
+// List will list all the users associated with a specified database instance,
+// along with their associated databases. This operation will not return any
+// system users or administrators for a database.
+func List(client *gophercloud.ServiceClient, instanceID string) pagination.Pager {
+ createPageFn := func(r pagination.PageResult) pagination.Page {
+ return UserPage{pagination.LinkedPageBase{PageResult: r}}
+ }
+
+ return pagination.NewPager(client, baseURL(client, instanceID), createPageFn)
+}
+
+// Delete will permanently delete a user from a specified database instance.
+func Delete(client *gophercloud.ServiceClient, instanceID, userName string) DeleteResult {
+ var res DeleteResult
+
+ _, res.Err = client.Request("DELETE", userURL(client, instanceID, userName), gophercloud.RequestOpts{
+ OkCodes: []int{202},
+ })
+
+ return res
+}
diff --git a/openstack/db/v1/users/requests_test.go b/openstack/db/v1/users/requests_test.go
new file mode 100644
index 0000000..5711f63
--- /dev/null
+++ b/openstack/db/v1/users/requests_test.go
@@ -0,0 +1,84 @@
+package users
+
+import (
+ "testing"
+
+ db "github.com/rackspace/gophercloud/openstack/db/v1/databases"
+ "github.com/rackspace/gophercloud/pagination"
+ th "github.com/rackspace/gophercloud/testhelper"
+ fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestCreate(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ HandleCreate(t)
+
+ opts := BatchCreateOpts{
+ CreateOpts{
+ Databases: db.BatchCreateOpts{
+ db.CreateOpts{Name: "databaseA"},
+ },
+ Name: "dbuser3",
+ Password: "secretsecret",
+ },
+ CreateOpts{
+ Databases: db.BatchCreateOpts{
+ db.CreateOpts{Name: "databaseB"},
+ db.CreateOpts{Name: "databaseC"},
+ },
+ Name: "dbuser4",
+ Password: "secretsecret",
+ },
+ }
+
+ res := Create(fake.ServiceClient(), instanceID, opts)
+ th.AssertNoErr(t, res.Err)
+}
+
+func TestUserList(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ HandleList(t)
+
+ expectedUsers := []User{
+ User{
+ Databases: []db.Database{
+ db.Database{Name: "databaseA"},
+ },
+ Name: "dbuser3",
+ },
+ User{
+ Databases: []db.Database{
+ db.Database{Name: "databaseB"},
+ db.Database{Name: "databaseC"},
+ },
+ Name: "dbuser4",
+ },
+ }
+
+ pages := 0
+ err := List(fake.ServiceClient(), instanceID).EachPage(func(page pagination.Page) (bool, error) {
+ pages++
+
+ actual, err := ExtractUsers(page)
+ if err != nil {
+ return false, err
+ }
+
+ th.CheckDeepEquals(t, expectedUsers, actual)
+ return true, nil
+ })
+
+ th.AssertNoErr(t, err)
+ th.AssertEquals(t, 1, pages)
+}
+
+func TestDelete(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ HandleDelete(t)
+
+ res := Delete(fake.ServiceClient(), instanceID, "{userName}")
+ th.AssertNoErr(t, res.Err)
+}
diff --git a/openstack/db/v1/users/results.go b/openstack/db/v1/users/results.go
new file mode 100644
index 0000000..217ddd8
--- /dev/null
+++ b/openstack/db/v1/users/results.go
@@ -0,0 +1,73 @@
+package users
+
+import (
+ "github.com/mitchellh/mapstructure"
+ "github.com/rackspace/gophercloud"
+ db "github.com/rackspace/gophercloud/openstack/db/v1/databases"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+// User represents a database user
+type User struct {
+ // The user name
+ Name string
+
+ // The user password
+ Password string
+
+ // The databases associated with this user
+ Databases []db.Database
+}
+
+// CreateResult represents the result of a create operation.
+type CreateResult struct {
+ gophercloud.ErrResult
+}
+
+// DeleteResult represents the result of a delete operation.
+type DeleteResult struct {
+ gophercloud.ErrResult
+}
+
+// UserPage represents a single page of a paginated user collection.
+type UserPage struct {
+ pagination.LinkedPageBase
+}
+
+// IsEmpty checks to see whether the collection is empty.
+func (page UserPage) IsEmpty() (bool, error) {
+ users, err := ExtractUsers(page)
+ if err != nil {
+ return true, err
+ }
+ return len(users) == 0, nil
+}
+
+// NextPageURL will retrieve the next page URL.
+func (page UserPage) NextPageURL() (string, error) {
+ type resp struct {
+ Links []gophercloud.Link `mapstructure:"users_links"`
+ }
+
+ var r resp
+ err := mapstructure.Decode(page.Body, &r)
+ if err != nil {
+ return "", err
+ }
+
+ return gophercloud.ExtractNextURL(r.Links)
+}
+
+// ExtractUsers will convert a generic pagination struct into a more
+// relevant slice of User structs.
+func ExtractUsers(page pagination.Page) ([]User, error) {
+ casted := page.(UserPage).Body
+
+ var response struct {
+ Users []User `mapstructure:"users"`
+ }
+
+ err := mapstructure.Decode(casted, &response)
+
+ return response.Users, err
+}
diff --git a/openstack/db/v1/users/urls.go b/openstack/db/v1/users/urls.go
new file mode 100644
index 0000000..2a3cacd
--- /dev/null
+++ b/openstack/db/v1/users/urls.go
@@ -0,0 +1,11 @@
+package users
+
+import "github.com/rackspace/gophercloud"
+
+func baseURL(c *gophercloud.ServiceClient, instanceID string) string {
+ return c.ServiceURL("instances", instanceID, "users")
+}
+
+func userURL(c *gophercloud.ServiceClient, instanceID, userName string) string {
+ return c.ServiceURL("instances", instanceID, "users", userName)
+}
diff --git a/openstack/networking/v2/extensions/layer3/floatingips/requests.go b/openstack/networking/v2/extensions/layer3/floatingips/requests.go
index 49d6f0b..29f752a 100644
--- a/openstack/networking/v2/extensions/layer3/floatingips/requests.go
+++ b/openstack/networking/v2/extensions/layer3/floatingips/requests.go
@@ -102,6 +102,7 @@
// Populate request body
reqBody := request{FloatingIP: floatingIP{
FloatingNetworkID: opts.FloatingNetworkID,
+ FloatingIP: opts.FloatingIP,
PortID: opts.PortID,
FixedIP: opts.FixedIP,
TenantID: opts.TenantID,
diff --git a/rackspace/client.go b/rackspace/client.go
index db3f305..a8f413e 100644
--- a/rackspace/client.go
+++ b/rackspace/client.go
@@ -212,3 +212,13 @@
}
return &gophercloud.ServiceClient{ProviderClient: client, Endpoint: url}, nil
}
+
+// NewDBV1 creates a ServiceClient that may be used to access the v1 DB service.
+func NewDBV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
+ eo.ApplyDefaults("rax:database")
+ url, err := client.EndpointLocator(eo)
+ if err != nil {
+ return nil, err
+ }
+ return &gophercloud.ServiceClient{ProviderClient: client, Endpoint: url}, nil
+}
diff --git a/rackspace/db/v1/backups/doc.go b/rackspace/db/v1/backups/doc.go
new file mode 100644
index 0000000..664eead
--- /dev/null
+++ b/rackspace/db/v1/backups/doc.go
@@ -0,0 +1,6 @@
+// Package backups provides information and interaction with the backup API
+// resource in the Rackspace Database service.
+//
+// A backup is a copy of a database instance that can be used to restore it to
+// some defined point in history.
+package backups
diff --git a/rackspace/db/v1/backups/fixtures.go b/rackspace/db/v1/backups/fixtures.go
new file mode 100644
index 0000000..45c2376
--- /dev/null
+++ b/rackspace/db/v1/backups/fixtures.go
@@ -0,0 +1,66 @@
+package backups
+
+import "time"
+
+var (
+ timestamp = "2015-11-12T14:22:42Z"
+ timeVal, _ = time.Parse(time.RFC3339, timestamp)
+)
+
+var getResp = `
+{
+ "backup": {
+ "created": "` + timestamp + `",
+ "description": "My Backup",
+ "id": "61f12fef-edb1-4561-8122-e7c00ef26a82",
+ "instance_id": "d4603f69-ec7e-4e9b-803f-600b9205576f",
+ "locationRef": null,
+ "name": "snapshot",
+ "parent_id": null,
+ "size": 100,
+ "status": "NEW",
+ "datastore": {
+ "version": "5.1",
+ "type": "MySQL",
+ "version_id": "20000000-0000-0000-0000-000000000002"
+ },
+ "updated": "` + timestamp + `"
+ }
+}
+`
+
+var createReq = `
+{
+ "backup": {
+ "description": "My Backup",
+ "instance": "d4603f69-ec7e-4e9b-803f-600b9205576f",
+ "name": "snapshot"
+ }
+}
+`
+
+var createResp = getResp
+
+var listResp = `
+{
+ "backups": [
+ {
+ "status": "COMPLETED",
+ "updated": "` + timestamp + `",
+ "description": "Backup from Restored Instance",
+ "datastore": {
+ "version": "5.1",
+ "type": "MySQL",
+ "version_id": "20000000-0000-0000-0000-000000000002"
+ },
+ "id": "87972694-4be2-40f5-83f8-501656e0032a",
+ "size": 0.141026,
+ "name": "restored_backup",
+ "created": "` + timestamp + `",
+ "instance_id": "29af2cd9-0674-48ab-b87a-b160f00208e6",
+ "parent_id": null,
+ "locationRef": "http://localhost/path/to/backup"
+ }
+ ]
+}
+`
diff --git a/rackspace/db/v1/backups/requests.go b/rackspace/db/v1/backups/requests.go
new file mode 100644
index 0000000..9170d78
--- /dev/null
+++ b/rackspace/db/v1/backups/requests.go
@@ -0,0 +1,138 @@
+package backups
+
+import (
+ "errors"
+
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+// CreateOptsBuilder is the top-level interface for creating JSON maps.
+type CreateOptsBuilder interface {
+ ToBackupCreateMap() (map[string]interface{}, error)
+}
+
+// CreateOpts is responsible for configuring newly provisioned backups.
+type CreateOpts struct {
+ // [REQUIRED] The name of the backup. The only restriction is the name must
+ // be less than 64 characters long.
+ Name string
+
+ // [REQUIRED] The ID of the instance being backed up.
+ InstanceID string
+
+ // [OPTIONAL] A human-readable explanation of the backup.
+ Description string
+}
+
+// ToBackupCreateMap will create a JSON map for the Create operation.
+func (opts CreateOpts) ToBackupCreateMap() (map[string]interface{}, error) {
+ if opts.Name == "" {
+ return nil, errors.New("Name is a required field")
+ }
+ if opts.InstanceID == "" {
+ return nil, errors.New("InstanceID is a required field")
+ }
+
+ backup := map[string]interface{}{
+ "name": opts.Name,
+ "instance": opts.InstanceID,
+ }
+
+ if opts.Description != "" {
+ backup["description"] = opts.Description
+ }
+
+ return map[string]interface{}{"backup": backup}, nil
+}
+
+// Create asynchronously creates a new backup for a specified database instance.
+// During the backup process, write access on MyISAM databases will be
+// temporarily disabled; innoDB databases will be unaffected. During this time,
+// you will not be able to add or delete databases or users; nor delete, stop
+// or reboot the instance itself. Only one backup is permitted at once.
+//
+// Backups are not deleted when database instances are deleted; you must
+// manually delete any backups created using Delete(). Backups are saved to your
+// Cloud Files account in a new container called z_CLOUDDB_BACKUPS. It is
+// strongly recommended you do not alter this container or its contents; usual
+// storage costs apply.
+func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) CreateResult {
+ var res CreateResult
+
+ reqBody, err := opts.ToBackupCreateMap()
+ if err != nil {
+ res.Err = err
+ return res
+ }
+
+ _, res.Err = client.Request("POST", baseURL(client), gophercloud.RequestOpts{
+ JSONBody: &reqBody,
+ JSONResponse: &res.Body,
+ OkCodes: []int{202},
+ })
+
+ return res
+}
+
+// ListOptsBuilder is the top-level interface for creating query strings.
+type ListOptsBuilder interface {
+ ToBackupListQuery() (string, error)
+}
+
+// ListOpts allows you to refine a list search by certain parameters.
+type ListOpts struct {
+ // The type of datastore by which to filter.
+ Datastore string `q:"datastore"`
+}
+
+// ToBackupListQuery converts a ListOpts struct into a query string.
+func (opts ListOpts) ToBackupListQuery() (string, error) {
+ q, err := gophercloud.BuildQueryString(opts)
+ if err != nil {
+ return "", err
+ }
+ return q.String(), nil
+}
+
+// List will list all the saved backups for all database instances.
+func List(client *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager {
+ url := baseURL(client)
+
+ if opts != nil {
+ query, err := opts.ToBackupListQuery()
+ if err != nil {
+ return pagination.Pager{Err: err}
+ }
+ url += query
+ }
+
+ pageFn := func(r pagination.PageResult) pagination.Page {
+ return BackupPage{pagination.SinglePageBase(r)}
+ }
+
+ return pagination.NewPager(client, url, pageFn)
+}
+
+// Get will retrieve details for a particular backup based on its unique ID.
+func Get(client *gophercloud.ServiceClient, id string) GetResult {
+ var res GetResult
+
+ _, res.Err = client.Request("GET", resourceURL(client, id), gophercloud.RequestOpts{
+ JSONResponse: &res.Body,
+ OkCodes: []int{200},
+ })
+
+ return res
+}
+
+// Delete will permanently delete a backup.
+func Delete(client *gophercloud.ServiceClient, id string) DeleteResult {
+ var res DeleteResult
+
+ _, res.Err = client.Request("DELETE", resourceURL(client, id), gophercloud.RequestOpts{
+ OkCodes: []int{202},
+ })
+
+ return res
+}
diff --git a/rackspace/db/v1/backups/requests_test.go b/rackspace/db/v1/backups/requests_test.go
new file mode 100644
index 0000000..d706733
--- /dev/null
+++ b/rackspace/db/v1/backups/requests_test.go
@@ -0,0 +1,131 @@
+package backups
+
+import (
+ "testing"
+
+ "github.com/rackspace/gophercloud/openstack/db/v1/datastores"
+ "github.com/rackspace/gophercloud/pagination"
+ th "github.com/rackspace/gophercloud/testhelper"
+ fake "github.com/rackspace/gophercloud/testhelper/client"
+ "github.com/rackspace/gophercloud/testhelper/fixture"
+)
+
+var (
+ backupID = "{backupID}"
+ _rootURL = "/backups"
+ resURL = _rootURL + "/" + backupID
+)
+
+func TestCreate(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ fixture.SetupHandler(t, _rootURL, "POST", createReq, createResp, 202)
+
+ opts := CreateOpts{
+ Name: "snapshot",
+ Description: "My Backup",
+ InstanceID: "d4603f69-ec7e-4e9b-803f-600b9205576f",
+ }
+
+ instance, err := Create(fake.ServiceClient(), opts).Extract()
+ th.AssertNoErr(t, err)
+
+ expected := &Backup{
+ Created: timeVal,
+ Description: "My Backup",
+ ID: "61f12fef-edb1-4561-8122-e7c00ef26a82",
+ InstanceID: "d4603f69-ec7e-4e9b-803f-600b9205576f",
+ LocationRef: "",
+ Name: "snapshot",
+ ParentID: "",
+ Size: 100,
+ Status: "NEW",
+ Updated: timeVal,
+ Datastore: datastores.DatastorePartial{
+ Version: "5.1",
+ Type: "MySQL",
+ VersionID: "20000000-0000-0000-0000-000000000002",
+ },
+ }
+
+ th.AssertDeepEquals(t, expected, instance)
+}
+
+func TestList(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ fixture.SetupHandler(t, _rootURL, "GET", "", listResp, 200)
+
+ pages := 0
+
+ err := List(fake.ServiceClient(), nil).EachPage(func(page pagination.Page) (bool, error) {
+ pages++
+ actual, err := ExtractBackups(page)
+ th.AssertNoErr(t, err)
+
+ expected := []Backup{
+ Backup{
+ Created: timeVal,
+ Description: "Backup from Restored Instance",
+ ID: "87972694-4be2-40f5-83f8-501656e0032a",
+ InstanceID: "29af2cd9-0674-48ab-b87a-b160f00208e6",
+ LocationRef: "http://localhost/path/to/backup",
+ Name: "restored_backup",
+ ParentID: "",
+ Size: 0.141026,
+ Status: "COMPLETED",
+ Updated: timeVal,
+ Datastore: datastores.DatastorePartial{
+ Version: "5.1",
+ Type: "MySQL",
+ VersionID: "20000000-0000-0000-0000-000000000002",
+ },
+ },
+ }
+
+ th.AssertDeepEquals(t, expected, actual)
+
+ return true, nil
+ })
+
+ th.AssertNoErr(t, err)
+ th.AssertEquals(t, 1, pages)
+}
+
+func TestGet(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ fixture.SetupHandler(t, resURL, "GET", "", getResp, 200)
+
+ instance, err := Get(fake.ServiceClient(), backupID).Extract()
+ th.AssertNoErr(t, err)
+
+ expected := &Backup{
+ Created: timeVal,
+ Description: "My Backup",
+ ID: "61f12fef-edb1-4561-8122-e7c00ef26a82",
+ InstanceID: "d4603f69-ec7e-4e9b-803f-600b9205576f",
+ LocationRef: "",
+ Name: "snapshot",
+ ParentID: "",
+ Size: 100,
+ Status: "NEW",
+ Updated: timeVal,
+ Datastore: datastores.DatastorePartial{
+ Version: "5.1",
+ Type: "MySQL",
+ VersionID: "20000000-0000-0000-0000-000000000002",
+ },
+ }
+
+ th.AssertDeepEquals(t, expected, instance)
+}
+
+func TestDelete(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ fixture.SetupHandler(t, resURL, "DELETE", "", "", 202)
+
+ err := Delete(fake.ServiceClient(), backupID).ExtractErr()
+ th.AssertNoErr(t, err)
+}
diff --git a/rackspace/db/v1/backups/results.go b/rackspace/db/v1/backups/results.go
new file mode 100644
index 0000000..82b551d
--- /dev/null
+++ b/rackspace/db/v1/backups/results.go
@@ -0,0 +1,141 @@
+package backups
+
+import (
+ "time"
+
+ "github.com/mitchellh/mapstructure"
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/openstack/db/v1/datastores"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+// Status represents the various states a Backup can be in.
+type Status string
+
+// Enum types for the status.
+const (
+ StatusNew Status = "NEW"
+ StatusBuilding Status = "BUILDING"
+ StatusCompleted Status = "COMPLETED"
+ StatusFailed Status = "FAILED"
+ StatusDeleteFailed Status = "DELETE_FAILED"
+)
+
+// Backup represents a Backup API resource.
+type Backup struct {
+ Description string
+ ID string
+ InstanceID string `json:"instance_id" mapstructure:"instance_id"`
+ LocationRef string
+ Name string
+ ParentID string `json:"parent_id" mapstructure:"parent_id"`
+ Size float64
+ Status Status
+ Created time.Time `mapstructure:"-"`
+ Updated time.Time `mapstructure:"-"`
+ Datastore datastores.DatastorePartial
+}
+
+// CreateResult represents the result of a create operation.
+type CreateResult struct {
+ commonResult
+}
+
+// GetResult represents the result of a get operation.
+type GetResult struct {
+ commonResult
+}
+
+// DeleteResult represents the result of a delete operation.
+type DeleteResult struct {
+ gophercloud.ErrResult
+}
+
+type commonResult struct {
+ gophercloud.Result
+}
+
+// Extract will retrieve a Backup struct from an operation's result.
+func (r commonResult) Extract() (*Backup, error) {
+ if r.Err != nil {
+ return nil, r.Err
+ }
+
+ var response struct {
+ Backup Backup `mapstructure:"backup"`
+ }
+
+ err := mapstructure.Decode(r.Body, &response)
+ val := r.Body.(map[string]interface{})["backup"].(map[string]interface{})
+
+ if t, ok := val["created"].(string); ok && t != "" {
+ creationTime, err := time.Parse(time.RFC3339, t)
+ if err != nil {
+ return &response.Backup, err
+ }
+ response.Backup.Created = creationTime
+ }
+
+ if t, ok := val["updated"].(string); ok && t != "" {
+ updatedTime, err := time.Parse(time.RFC3339, t)
+ if err != nil {
+ return &response.Backup, err
+ }
+ response.Backup.Updated = updatedTime
+ }
+
+ return &response.Backup, err
+}
+
+// BackupPage represents a page of backups.
+type BackupPage struct {
+ pagination.SinglePageBase
+}
+
+// IsEmpty checks whether an BackupPage struct is empty.
+func (r BackupPage) IsEmpty() (bool, error) {
+ is, err := ExtractBackups(r)
+ if err != nil {
+ return true, err
+ }
+ return len(is) == 0, nil
+}
+
+// ExtractBackups will retrieve a slice of Backup structs from a paginated collection.
+func ExtractBackups(page pagination.Page) ([]Backup, error) {
+ casted := page.(BackupPage).Body
+
+ var resp struct {
+ Backups []Backup `mapstructure:"backups" json:"backups"`
+ }
+
+ err := mapstructure.Decode(casted, &resp)
+
+ var vals []interface{}
+ switch (casted).(type) {
+ case interface{}:
+ vals = casted.(map[string]interface{})["backups"].([]interface{})
+ }
+
+ for i, v := range vals {
+ val := v.(map[string]interface{})
+
+ if t, ok := val["created"].(string); ok && t != "" {
+ creationTime, err := time.Parse(time.RFC3339, t)
+ if err != nil {
+ return resp.Backups, err
+ }
+ resp.Backups[i].Created = creationTime
+ }
+
+ if t, ok := val["updated"].(string); ok && t != "" {
+ updatedTime, err := time.Parse(time.RFC3339, t)
+ if err != nil {
+ return resp.Backups, err
+ }
+ resp.Backups[i].Updated = updatedTime
+ }
+ }
+
+ return resp.Backups, err
+}
diff --git a/rackspace/db/v1/backups/urls.go b/rackspace/db/v1/backups/urls.go
new file mode 100644
index 0000000..553444e
--- /dev/null
+++ b/rackspace/db/v1/backups/urls.go
@@ -0,0 +1,11 @@
+package backups
+
+import "github.com/rackspace/gophercloud"
+
+func baseURL(c *gophercloud.ServiceClient) string {
+ return c.ServiceURL("backups")
+}
+
+func resourceURL(c *gophercloud.ServiceClient, backupID string) string {
+ return c.ServiceURL("backups", backupID)
+}
diff --git a/rackspace/db/v1/configurations/delegate.go b/rackspace/db/v1/configurations/delegate.go
new file mode 100644
index 0000000..d8cb48a
--- /dev/null
+++ b/rackspace/db/v1/configurations/delegate.go
@@ -0,0 +1,79 @@
+package configurations
+
+import (
+ "github.com/rackspace/gophercloud"
+ os "github.com/rackspace/gophercloud/openstack/db/v1/configurations"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+// List will list all of the available configurations.
+func List(client *gophercloud.ServiceClient) pagination.Pager {
+ return os.List(client)
+}
+
+// Create will create a new configuration group.
+func Create(client *gophercloud.ServiceClient, opts os.CreateOptsBuilder) os.CreateResult {
+ return os.Create(client, opts)
+}
+
+// Get will retrieve the details for a specified configuration group.
+func Get(client *gophercloud.ServiceClient, configID string) os.GetResult {
+ return os.Get(client, configID)
+}
+
+// Update will modify an existing configuration group by performing a merge
+// between new and existing values. If the key already exists, the new value
+// will overwrite. All other keys will remain unaffected.
+func Update(client *gophercloud.ServiceClient, configID string, opts os.UpdateOptsBuilder) os.UpdateResult {
+ return os.Update(client, configID, opts)
+}
+
+// Replace will modify an existing configuration group by overwriting the
+// entire parameter group with the new values provided. Any existing keys not
+// included in UpdateOptsBuilder will be deleted.
+func Replace(client *gophercloud.ServiceClient, configID string, opts os.UpdateOptsBuilder) os.ReplaceResult {
+ return os.Replace(client, configID, opts)
+}
+
+// Delete will permanently delete a configuration group. Please note that
+// config groups cannot be deleted whilst still attached to running instances -
+// you must detach and then delete them.
+func Delete(client *gophercloud.ServiceClient, configID string) os.DeleteResult {
+ return os.Delete(client, configID)
+}
+
+// ListInstances will list all the instances associated with a particular
+// configuration group.
+func ListInstances(client *gophercloud.ServiceClient, configID string) pagination.Pager {
+ return os.ListInstances(client, configID)
+}
+
+// ListDatastoreParams will list all the available and supported parameters
+// that can be used for a particular datastore ID and a particular version.
+// For example, if you are wondering how you can configure a MySQL 5.6 instance,
+// you can use this operation (you will need to retrieve the MySQL datastore ID
+// by using the datastores API).
+func ListDatastoreParams(client *gophercloud.ServiceClient, datastoreID, versionID string) pagination.Pager {
+ return os.ListDatastoreParams(client, datastoreID, versionID)
+}
+
+// GetDatastoreParam will retrieve information about a specific configuration
+// parameter. For example, you can use this operation to understand more about
+// "innodb_file_per_table" configuration param for MySQL datastores. You will
+// need the param's ID first, which can be attained by using the ListDatastoreParams
+// operation.
+func GetDatastoreParam(client *gophercloud.ServiceClient, datastoreID, versionID, paramID string) os.ParamResult {
+ return os.GetDatastoreParam(client, datastoreID, versionID, paramID)
+}
+
+// ListGlobalParams is similar to ListDatastoreParams but does not require a
+// DatastoreID.
+func ListGlobalParams(client *gophercloud.ServiceClient, versionID string) pagination.Pager {
+ return os.ListGlobalParams(client, versionID)
+}
+
+// GetGlobalParam is similar to GetDatastoreParam but does not require a
+// DatastoreID.
+func GetGlobalParam(client *gophercloud.ServiceClient, versionID, paramID string) os.ParamResult {
+ return os.GetGlobalParam(client, versionID, paramID)
+}
diff --git a/rackspace/db/v1/configurations/delegate_test.go b/rackspace/db/v1/configurations/delegate_test.go
new file mode 100644
index 0000000..580f02a
--- /dev/null
+++ b/rackspace/db/v1/configurations/delegate_test.go
@@ -0,0 +1,237 @@
+package configurations
+
+import (
+ "testing"
+
+ os "github.com/rackspace/gophercloud/openstack/db/v1/configurations"
+ "github.com/rackspace/gophercloud/pagination"
+ "github.com/rackspace/gophercloud/rackspace/db/v1/instances"
+ th "github.com/rackspace/gophercloud/testhelper"
+ fake "github.com/rackspace/gophercloud/testhelper/client"
+ "github.com/rackspace/gophercloud/testhelper/fixture"
+)
+
+var (
+ configID = "{configID}"
+ _baseURL = "/configurations"
+ resURL = _baseURL + "/" + configID
+
+ dsID = "{datastoreID}"
+ versionID = "{versionID}"
+ paramID = "{paramID}"
+ dsParamListURL = "/datastores/" + dsID + "/versions/" + versionID + "/parameters"
+ dsParamGetURL = "/datastores/" + dsID + "/versions/" + versionID + "/parameters/" + paramID
+ globalParamListURL = "/datastores/versions/" + versionID + "/parameters"
+ globalParamGetURL = "/datastores/versions/" + versionID + "/parameters/" + paramID
+)
+
+func TestList(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ fixture.SetupHandler(t, _baseURL, "GET", "", listConfigsJSON, 200)
+
+ count := 0
+ err := List(fake.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) {
+ count++
+ actual, err := os.ExtractConfigs(page)
+ th.AssertNoErr(t, err)
+
+ expected := []os.Config{exampleConfig}
+ th.AssertDeepEquals(t, expected, actual)
+
+ return true, nil
+ })
+
+ th.AssertEquals(t, 1, count)
+ th.AssertNoErr(t, err)
+}
+
+func TestGet(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ fixture.SetupHandler(t, resURL, "GET", "", getConfigJSON, 200)
+
+ config, err := Get(fake.ServiceClient(), configID).Extract()
+ th.AssertNoErr(t, err)
+ th.AssertDeepEquals(t, &exampleConfig, config)
+}
+
+func TestCreate(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ fixture.SetupHandler(t, _baseURL, "POST", createReq, createConfigJSON, 200)
+
+ opts := os.CreateOpts{
+ Datastore: &os.DatastoreOpts{
+ Type: "a00000a0-00a0-0a00-00a0-000a000000aa",
+ Version: "b00000b0-00b0-0b00-00b0-000b000000bb",
+ },
+ Description: "example description",
+ Name: "example-configuration-name",
+ Values: map[string]interface{}{
+ "collation_server": "latin1_swedish_ci",
+ "connect_timeout": 120,
+ },
+ }
+
+ config, err := Create(fake.ServiceClient(), opts).Extract()
+ th.AssertNoErr(t, err)
+ th.AssertDeepEquals(t, &exampleConfigWithValues, config)
+}
+
+func TestUpdate(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ fixture.SetupHandler(t, resURL, "PATCH", updateReq, "", 200)
+
+ opts := os.UpdateOpts{
+ Values: map[string]interface{}{
+ "connect_timeout": 300,
+ },
+ }
+
+ err := Update(fake.ServiceClient(), configID, opts).ExtractErr()
+ th.AssertNoErr(t, err)
+}
+
+func TestReplace(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ fixture.SetupHandler(t, resURL, "PUT", updateReq, "", 202)
+
+ opts := os.UpdateOpts{
+ Values: map[string]interface{}{
+ "connect_timeout": 300,
+ },
+ }
+
+ err := Replace(fake.ServiceClient(), configID, opts).ExtractErr()
+ th.AssertNoErr(t, err)
+}
+
+func TestDelete(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ fixture.SetupHandler(t, resURL, "DELETE", "", "", 202)
+
+ err := Delete(fake.ServiceClient(), configID).ExtractErr()
+ th.AssertNoErr(t, err)
+}
+
+func TestListInstances(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ fixture.SetupHandler(t, resURL+"/instances", "GET", "", listInstancesJSON, 200)
+
+ expectedInstance := instances.Instance{
+ ID: "d4603f69-ec7e-4e9b-803f-600b9205576f",
+ Name: "json_rack_instance",
+ }
+
+ pages := 0
+ err := ListInstances(fake.ServiceClient(), configID).EachPage(func(page pagination.Page) (bool, error) {
+ pages++
+
+ actual, err := instances.ExtractInstances(page)
+ if err != nil {
+ return false, err
+ }
+
+ th.AssertDeepEquals(t, actual, []instances.Instance{expectedInstance})
+
+ return true, nil
+ })
+
+ th.AssertNoErr(t, err)
+ th.AssertEquals(t, 1, pages)
+}
+
+func TestListDSParams(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ fixture.SetupHandler(t, dsParamListURL, "GET", "", listParamsJSON, 200)
+
+ pages := 0
+ err := ListDatastoreParams(fake.ServiceClient(), dsID, versionID).EachPage(func(page pagination.Page) (bool, error) {
+ pages++
+
+ actual, err := os.ExtractParams(page)
+ if err != nil {
+ return false, err
+ }
+
+ expected := []os.Param{
+ os.Param{Max: 1, Min: 0, Name: "innodb_file_per_table", RestartRequired: true, Type: "integer"},
+ os.Param{Max: 4294967296, Min: 0, Name: "key_buffer_size", RestartRequired: false, Type: "integer"},
+ os.Param{Max: 65535, Min: 2, Name: "connect_timeout", RestartRequired: false, Type: "integer"},
+ os.Param{Max: 4294967296, Min: 0, Name: "join_buffer_size", RestartRequired: false, Type: "integer"},
+ }
+
+ th.AssertDeepEquals(t, actual, expected)
+
+ return true, nil
+ })
+
+ th.AssertNoErr(t, err)
+ th.AssertEquals(t, 1, pages)
+}
+
+func TestGetDSParam(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ fixture.SetupHandler(t, dsParamGetURL, "GET", "", getParamJSON, 200)
+
+ param, err := GetDatastoreParam(fake.ServiceClient(), dsID, versionID, paramID).Extract()
+ th.AssertNoErr(t, err)
+
+ expected := &os.Param{
+ Max: 1, Min: 0, Name: "innodb_file_per_table", RestartRequired: true, Type: "integer",
+ }
+
+ th.AssertDeepEquals(t, expected, param)
+}
+
+func TestListGlobalParams(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ fixture.SetupHandler(t, globalParamListURL, "GET", "", listParamsJSON, 200)
+
+ pages := 0
+ err := ListGlobalParams(fake.ServiceClient(), versionID).EachPage(func(page pagination.Page) (bool, error) {
+ pages++
+
+ actual, err := os.ExtractParams(page)
+ if err != nil {
+ return false, err
+ }
+
+ expected := []os.Param{
+ os.Param{Max: 1, Min: 0, Name: "innodb_file_per_table", RestartRequired: true, Type: "integer"},
+ os.Param{Max: 4294967296, Min: 0, Name: "key_buffer_size", RestartRequired: false, Type: "integer"},
+ os.Param{Max: 65535, Min: 2, Name: "connect_timeout", RestartRequired: false, Type: "integer"},
+ os.Param{Max: 4294967296, Min: 0, Name: "join_buffer_size", RestartRequired: false, Type: "integer"},
+ }
+
+ th.AssertDeepEquals(t, actual, expected)
+
+ return true, nil
+ })
+
+ th.AssertNoErr(t, err)
+ th.AssertEquals(t, 1, pages)
+}
+
+func TestGetGlobalParam(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ fixture.SetupHandler(t, globalParamGetURL, "GET", "", getParamJSON, 200)
+
+ param, err := GetGlobalParam(fake.ServiceClient(), versionID, paramID).Extract()
+ th.AssertNoErr(t, err)
+
+ expected := &os.Param{
+ Max: 1, Min: 0, Name: "innodb_file_per_table", RestartRequired: true, Type: "integer",
+ }
+
+ th.AssertDeepEquals(t, expected, param)
+}
diff --git a/rackspace/db/v1/configurations/doc.go b/rackspace/db/v1/configurations/doc.go
new file mode 100644
index 0000000..48c51d6
--- /dev/null
+++ b/rackspace/db/v1/configurations/doc.go
@@ -0,0 +1 @@
+package configurations
diff --git a/rackspace/db/v1/configurations/fixtures.go b/rackspace/db/v1/configurations/fixtures.go
new file mode 100644
index 0000000..d8a2233
--- /dev/null
+++ b/rackspace/db/v1/configurations/fixtures.go
@@ -0,0 +1,159 @@
+package configurations
+
+import (
+ "fmt"
+ "time"
+
+ os "github.com/rackspace/gophercloud/openstack/db/v1/configurations"
+)
+
+var (
+ timestamp = "2015-11-12T14:22:42Z"
+ timeVal, _ = time.Parse(time.RFC3339, timestamp)
+)
+
+var singleConfigJSON = `
+{
+ "created": "` + timestamp + `",
+ "datastore_name": "mysql",
+ "datastore_version_id": "b00000b0-00b0-0b00-00b0-000b000000bb",
+ "datastore_version_name": "5.6",
+ "description": "example_description",
+ "id": "005a8bb7-a8df-40ee-b0b7-fc144641abc2",
+ "name": "example-configuration-name",
+ "updated": "` + timestamp + `"
+}
+`
+
+var singleConfigWithValuesJSON = `
+{
+ "created": "` + timestamp + `",
+ "datastore_name": "mysql",
+ "datastore_version_id": "b00000b0-00b0-0b00-00b0-000b000000bb",
+ "datastore_version_name": "5.6",
+ "description": "example description",
+ "id": "005a8bb7-a8df-40ee-b0b7-fc144641abc2",
+ "instance_count": 0,
+ "name": "example-configuration-name",
+ "updated": "` + timestamp + `",
+ "values": {
+ "collation_server": "latin1_swedish_ci",
+ "connect_timeout": 120
+ }
+}
+`
+
+var (
+ listConfigsJSON = fmt.Sprintf(`{"configurations": [%s]}`, singleConfigJSON)
+ getConfigJSON = fmt.Sprintf(`{"configuration": %s}`, singleConfigJSON)
+ createConfigJSON = fmt.Sprintf(`{"configuration": %s}`, singleConfigWithValuesJSON)
+)
+
+var createReq = `
+{
+ "configuration": {
+ "datastore": {
+ "type": "a00000a0-00a0-0a00-00a0-000a000000aa",
+ "version": "b00000b0-00b0-0b00-00b0-000b000000bb"
+ },
+ "description": "example description",
+ "name": "example-configuration-name",
+ "values": {
+ "collation_server": "latin1_swedish_ci",
+ "connect_timeout": 120
+ }
+ }
+}
+`
+
+var updateReq = `
+{
+ "configuration": {
+ "values": {
+ "connect_timeout": 300
+ }
+ }
+}
+`
+
+var listInstancesJSON = `
+{
+ "instances": [
+ {
+ "id": "d4603f69-ec7e-4e9b-803f-600b9205576f",
+ "name": "json_rack_instance"
+ }
+ ]
+}
+`
+
+var listParamsJSON = `
+{
+ "configuration-parameters": [
+ {
+ "max": 1,
+ "min": 0,
+ "name": "innodb_file_per_table",
+ "restart_required": true,
+ "type": "integer"
+ },
+ {
+ "max": 4294967296,
+ "min": 0,
+ "name": "key_buffer_size",
+ "restart_required": false,
+ "type": "integer"
+ },
+ {
+ "max": 65535,
+ "min": 2,
+ "name": "connect_timeout",
+ "restart_required": false,
+ "type": "integer"
+ },
+ {
+ "max": 4294967296,
+ "min": 0,
+ "name": "join_buffer_size",
+ "restart_required": false,
+ "type": "integer"
+ }
+ ]
+}
+`
+
+var getParamJSON = `
+{
+ "max": 1,
+ "min": 0,
+ "name": "innodb_file_per_table",
+ "restart_required": true,
+ "type": "integer"
+}
+`
+
+var exampleConfig = os.Config{
+ Created: timeVal,
+ DatastoreName: "mysql",
+ DatastoreVersionID: "b00000b0-00b0-0b00-00b0-000b000000bb",
+ DatastoreVersionName: "5.6",
+ Description: "example_description",
+ ID: "005a8bb7-a8df-40ee-b0b7-fc144641abc2",
+ Name: "example-configuration-name",
+ Updated: timeVal,
+}
+
+var exampleConfigWithValues = os.Config{
+ Created: timeVal,
+ DatastoreName: "mysql",
+ DatastoreVersionID: "b00000b0-00b0-0b00-00b0-000b000000bb",
+ DatastoreVersionName: "5.6",
+ Description: "example description",
+ ID: "005a8bb7-a8df-40ee-b0b7-fc144641abc2",
+ Name: "example-configuration-name",
+ Updated: timeVal,
+ Values: map[string]interface{}{
+ "collation_server": "latin1_swedish_ci",
+ "connect_timeout": 120,
+ },
+}
diff --git a/rackspace/db/v1/databases/delegate.go b/rackspace/db/v1/databases/delegate.go
new file mode 100644
index 0000000..56552d1
--- /dev/null
+++ b/rackspace/db/v1/databases/delegate.go
@@ -0,0 +1,19 @@
+package databases
+
+import (
+ "github.com/rackspace/gophercloud"
+ os "github.com/rackspace/gophercloud/openstack/db/v1/databases"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+func Create(client *gophercloud.ServiceClient, instanceID string, opts os.CreateOptsBuilder) os.CreateResult {
+ return os.Create(client, instanceID, opts)
+}
+
+func List(client *gophercloud.ServiceClient, instanceID string) pagination.Pager {
+ return os.List(client, instanceID)
+}
+
+func Delete(client *gophercloud.ServiceClient, instanceID, dbName string) os.DeleteResult {
+ return os.Delete(client, instanceID, dbName)
+}
diff --git a/rackspace/db/v1/databases/delegate_test.go b/rackspace/db/v1/databases/delegate_test.go
new file mode 100644
index 0000000..b9e50a5
--- /dev/null
+++ b/rackspace/db/v1/databases/delegate_test.go
@@ -0,0 +1,71 @@
+package databases
+
+import (
+ "testing"
+
+ os "github.com/rackspace/gophercloud/openstack/db/v1/databases"
+ "github.com/rackspace/gophercloud/pagination"
+ th "github.com/rackspace/gophercloud/testhelper"
+ fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+var (
+ instanceID = "{instanceID}"
+ rootURL = "/instances"
+ resURL = rootURL + "/" + instanceID
+ uRootURL = resURL + "/root"
+ aURL = resURL + "/action"
+)
+
+func TestCreate(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ os.HandleCreate(t)
+
+ opts := os.BatchCreateOpts{
+ os.CreateOpts{Name: "testingdb", CharSet: "utf8", Collate: "utf8_general_ci"},
+ os.CreateOpts{Name: "sampledb"},
+ }
+
+ res := Create(fake.ServiceClient(), instanceID, opts)
+ th.AssertNoErr(t, res.Err)
+}
+
+func TestList(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ os.HandleList(t)
+
+ expectedDBs := []os.Database{
+ os.Database{Name: "anotherexampledb"},
+ os.Database{Name: "exampledb"},
+ os.Database{Name: "nextround"},
+ os.Database{Name: "sampledb"},
+ os.Database{Name: "testingdb"},
+ }
+
+ pages := 0
+ err := List(fake.ServiceClient(), instanceID).EachPage(func(page pagination.Page) (bool, error) {
+ pages++
+
+ actual, err := os.ExtractDBs(page)
+ if err != nil {
+ return false, err
+ }
+
+ th.CheckDeepEquals(t, expectedDBs, actual)
+ return true, nil
+ })
+
+ th.AssertNoErr(t, err)
+ th.AssertEquals(t, 1, pages)
+}
+
+func TestDelete(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ os.HandleDelete(t)
+
+ err := os.Delete(fake.ServiceClient(), instanceID, "{dbName}").ExtractErr()
+ th.AssertNoErr(t, err)
+}
diff --git a/rackspace/db/v1/databases/doc.go b/rackspace/db/v1/databases/doc.go
new file mode 100644
index 0000000..1a178b6
--- /dev/null
+++ b/rackspace/db/v1/databases/doc.go
@@ -0,0 +1,3 @@
+// Package databases provides information and interaction with the database API
+// resource in the Rackspace Database service.
+package databases
diff --git a/rackspace/db/v1/databases/urls.go b/rackspace/db/v1/databases/urls.go
new file mode 100644
index 0000000..18cbec7
--- /dev/null
+++ b/rackspace/db/v1/databases/urls.go
@@ -0,0 +1 @@
+package databases
diff --git a/rackspace/db/v1/datastores/delegate.go b/rackspace/db/v1/datastores/delegate.go
new file mode 100644
index 0000000..573496d
--- /dev/null
+++ b/rackspace/db/v1/datastores/delegate.go
@@ -0,0 +1,28 @@
+package datastores
+
+import (
+ "github.com/rackspace/gophercloud"
+ os "github.com/rackspace/gophercloud/openstack/db/v1/datastores"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+// List will list all available flavors.
+func List(client *gophercloud.ServiceClient) pagination.Pager {
+ return os.List(client)
+}
+
+// Get retrieves the details for a particular flavor.
+func Get(client *gophercloud.ServiceClient, flavorID string) os.GetResult {
+ return os.Get(client, flavorID)
+}
+
+// ListVersions will list all of the available versions for a specified
+// datastore type.
+func ListVersions(client *gophercloud.ServiceClient, datastoreID string) pagination.Pager {
+ return os.ListVersions(client, datastoreID)
+}
+
+// GetVersion will retrieve the details of a specified datastore version.
+func GetVersion(client *gophercloud.ServiceClient, datastoreID, versionID string) os.GetVersionResult {
+ return os.GetVersion(client, datastoreID, versionID)
+}
diff --git a/rackspace/db/v1/datastores/delegate_test.go b/rackspace/db/v1/datastores/delegate_test.go
new file mode 100644
index 0000000..71111b9
--- /dev/null
+++ b/rackspace/db/v1/datastores/delegate_test.go
@@ -0,0 +1,79 @@
+package datastores
+
+import (
+ "testing"
+
+ os "github.com/rackspace/gophercloud/openstack/db/v1/datastores"
+ "github.com/rackspace/gophercloud/pagination"
+ th "github.com/rackspace/gophercloud/testhelper"
+ fake "github.com/rackspace/gophercloud/testhelper/client"
+ "github.com/rackspace/gophercloud/testhelper/fixture"
+)
+
+func TestList(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ fixture.SetupHandler(t, "/datastores", "GET", "", os.ListDSResp, 200)
+
+ pages := 0
+
+ err := List(fake.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) {
+ pages++
+
+ actual, err := os.ExtractDatastores(page)
+ if err != nil {
+ return false, err
+ }
+
+ th.CheckDeepEquals(t, []os.Datastore{os.ExampleDatastore}, actual)
+
+ return true, nil
+ })
+
+ th.AssertNoErr(t, err)
+ th.AssertEquals(t, 1, pages)
+}
+
+func TestGet(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ fixture.SetupHandler(t, "/datastores/{dsID}", "GET", "", os.GetDSResp, 200)
+
+ ds, err := Get(fake.ServiceClient(), "{dsID}").Extract()
+ th.AssertNoErr(t, err)
+ th.AssertDeepEquals(t, &os.ExampleDatastore, ds)
+}
+
+func TestListVersions(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ fixture.SetupHandler(t, "/datastores/{dsID}/versions", "GET", "", os.ListVersionsResp, 200)
+
+ pages := 0
+
+ err := ListVersions(fake.ServiceClient(), "{dsID}").EachPage(func(page pagination.Page) (bool, error) {
+ pages++
+
+ actual, err := os.ExtractVersions(page)
+ if err != nil {
+ return false, err
+ }
+
+ th.CheckDeepEquals(t, os.ExampleVersions, actual)
+
+ return true, nil
+ })
+
+ th.AssertNoErr(t, err)
+ th.AssertEquals(t, 1, pages)
+}
+
+func TestGetVersion(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ fixture.SetupHandler(t, "/datastores/{dsID}/versions/{versionID}", "GET", "", os.GetVersionResp, 200)
+
+ ds, err := GetVersion(fake.ServiceClient(), "{dsID}", "{versionID}").Extract()
+ th.AssertNoErr(t, err)
+ th.AssertDeepEquals(t, &os.ExampleVersion1, ds)
+}
diff --git a/rackspace/db/v1/datastores/doc.go b/rackspace/db/v1/datastores/doc.go
new file mode 100644
index 0000000..f36997a
--- /dev/null
+++ b/rackspace/db/v1/datastores/doc.go
@@ -0,0 +1 @@
+package datastores
diff --git a/rackspace/db/v1/flavors/delegate.go b/rackspace/db/v1/flavors/delegate.go
new file mode 100644
index 0000000..689b81e
--- /dev/null
+++ b/rackspace/db/v1/flavors/delegate.go
@@ -0,0 +1,17 @@
+package flavors
+
+import (
+ "github.com/rackspace/gophercloud"
+ os "github.com/rackspace/gophercloud/openstack/db/v1/flavors"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+// List will list all available flavors.
+func List(client *gophercloud.ServiceClient) pagination.Pager {
+ return os.List(client)
+}
+
+// Get retrieves the details for a particular flavor.
+func Get(client *gophercloud.ServiceClient, flavorID string) os.GetResult {
+ return os.Get(client, flavorID)
+}
diff --git a/rackspace/db/v1/flavors/delegate_test.go b/rackspace/db/v1/flavors/delegate_test.go
new file mode 100644
index 0000000..f5f6442
--- /dev/null
+++ b/rackspace/db/v1/flavors/delegate_test.go
@@ -0,0 +1,95 @@
+package flavors
+
+import (
+ "testing"
+
+ "github.com/rackspace/gophercloud"
+ os "github.com/rackspace/gophercloud/openstack/db/v1/flavors"
+ "github.com/rackspace/gophercloud/pagination"
+ th "github.com/rackspace/gophercloud/testhelper"
+ fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestListFlavors(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ os.HandleList(t)
+
+ pages := 0
+ err := List(fake.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) {
+ pages++
+
+ actual, err := os.ExtractFlavors(page)
+ if err != nil {
+ return false, err
+ }
+
+ expected := []os.Flavor{
+ os.Flavor{
+ ID: "1",
+ Name: "m1.tiny",
+ RAM: 512,
+ Links: []gophercloud.Link{
+ gophercloud.Link{Href: "https://openstack.example.com/v1.0/1234/flavors/1", Rel: "self"},
+ gophercloud.Link{Href: "https://openstack.example.com/flavors/1", Rel: "bookmark"},
+ },
+ },
+ os.Flavor{
+ ID: "2",
+ Name: "m1.small",
+ RAM: 1024,
+ Links: []gophercloud.Link{
+ gophercloud.Link{Href: "https://openstack.example.com/v1.0/1234/flavors/2", Rel: "self"},
+ gophercloud.Link{Href: "https://openstack.example.com/flavors/2", Rel: "bookmark"},
+ },
+ },
+ os.Flavor{
+ ID: "3",
+ Name: "m1.medium",
+ RAM: 2048,
+ Links: []gophercloud.Link{
+ gophercloud.Link{Href: "https://openstack.example.com/v1.0/1234/flavors/3", Rel: "self"},
+ gophercloud.Link{Href: "https://openstack.example.com/flavors/3", Rel: "bookmark"},
+ },
+ },
+ os.Flavor{
+ ID: "4",
+ Name: "m1.large",
+ RAM: 4096,
+ Links: []gophercloud.Link{
+ gophercloud.Link{Href: "https://openstack.example.com/v1.0/1234/flavors/4", Rel: "self"},
+ gophercloud.Link{Href: "https://openstack.example.com/flavors/4", Rel: "bookmark"},
+ },
+ },
+ }
+
+ th.AssertDeepEquals(t, expected, actual)
+
+ return true, nil
+ })
+
+ th.AssertNoErr(t, err)
+ if pages != 1 {
+ t.Errorf("Expected one page, got %d", pages)
+ }
+}
+
+func TestGetFlavor(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ os.HandleGet(t)
+
+ actual, err := Get(fake.ServiceClient(), "{flavorID}").Extract()
+ th.AssertNoErr(t, err)
+
+ expected := &os.Flavor{
+ ID: "1",
+ Name: "m1.tiny",
+ RAM: 512,
+ Links: []gophercloud.Link{
+ gophercloud.Link{Href: "https://openstack.example.com/v1.0/1234/flavors/1", Rel: "self"},
+ },
+ }
+
+ th.AssertDeepEquals(t, expected, actual)
+}
diff --git a/rackspace/db/v1/flavors/doc.go b/rackspace/db/v1/flavors/doc.go
new file mode 100644
index 0000000..922a4e6
--- /dev/null
+++ b/rackspace/db/v1/flavors/doc.go
@@ -0,0 +1,3 @@
+// Package flavors provides information and interaction with the flavor API
+// resource in the Rackspace Database service.
+package flavors
diff --git a/rackspace/db/v1/instances/delegate.go b/rackspace/db/v1/instances/delegate.go
new file mode 100644
index 0000000..f2656fe
--- /dev/null
+++ b/rackspace/db/v1/instances/delegate.go
@@ -0,0 +1,49 @@
+package instances
+
+import (
+ "github.com/rackspace/gophercloud"
+ os "github.com/rackspace/gophercloud/openstack/db/v1/instances"
+)
+
+// Get retrieves the status and information for a specified database instance.
+func Get(client *gophercloud.ServiceClient, id string) GetResult {
+ return GetResult{os.Get(client, id)}
+}
+
+// Delete permanently destroys the database instance.
+func Delete(client *gophercloud.ServiceClient, id string) os.DeleteResult {
+ return os.Delete(client, id)
+}
+
+// EnableRootUser enables the login from any host for the root user and
+// provides the user with a generated root password.
+func EnableRootUser(client *gophercloud.ServiceClient, id string) os.UserRootResult {
+ return os.EnableRootUser(client, id)
+}
+
+// IsRootEnabled checks an instance to see if root access is enabled. It returns
+// True if root user is enabled for the specified database instance or False
+// otherwise.
+func IsRootEnabled(client *gophercloud.ServiceClient, id string) (bool, error) {
+ return os.IsRootEnabled(client, id)
+}
+
+// Restart will restart only the MySQL Instance. Restarting MySQL will
+// erase any dynamic configuration settings that you have made within MySQL.
+// The MySQL service will be unavailable until the instance restarts.
+func Restart(client *gophercloud.ServiceClient, id string) os.ActionResult {
+ return os.Restart(client, id)
+}
+
+// Resize changes the memory size of the instance, assuming a valid
+// flavorRef is provided. It will also restart the MySQL service.
+func Resize(client *gophercloud.ServiceClient, id, flavorRef string) os.ActionResult {
+ return os.Resize(client, id, flavorRef)
+}
+
+// ResizeVolume will resize the attached volume for an instance. It supports
+// only increasing the volume size and does not support decreasing the size.
+// The volume size is in gigabytes (GB) and must be an integer.
+func ResizeVolume(client *gophercloud.ServiceClient, id string, size int) os.ActionResult {
+ return os.ResizeVolume(client, id, size)
+}
diff --git a/rackspace/db/v1/instances/delegate_test.go b/rackspace/db/v1/instances/delegate_test.go
new file mode 100644
index 0000000..716e0a4
--- /dev/null
+++ b/rackspace/db/v1/instances/delegate_test.go
@@ -0,0 +1,107 @@
+package instances
+
+import (
+ "testing"
+
+ osDBs "github.com/rackspace/gophercloud/openstack/db/v1/databases"
+ os "github.com/rackspace/gophercloud/openstack/db/v1/instances"
+ osUsers "github.com/rackspace/gophercloud/openstack/db/v1/users"
+ th "github.com/rackspace/gophercloud/testhelper"
+ fake "github.com/rackspace/gophercloud/testhelper/client"
+ "github.com/rackspace/gophercloud/testhelper/fixture"
+)
+
+var (
+ _rootURL = "/instances"
+ resURL = "/instances/" + instanceID
+)
+
+func TestCreate(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ fixture.SetupHandler(t, _rootURL, "POST", createReq, createResp, 200)
+
+ opts := CreateOpts{
+ Name: "json_rack_instance",
+ FlavorRef: "1",
+ Databases: osDBs.BatchCreateOpts{
+ osDBs.CreateOpts{CharSet: "utf8", Collate: "utf8_general_ci", Name: "sampledb"},
+ osDBs.CreateOpts{Name: "nextround"},
+ },
+ Users: osUsers.BatchCreateOpts{
+ osUsers.CreateOpts{
+ Name: "demouser",
+ Password: "demopassword",
+ Databases: osDBs.BatchCreateOpts{
+ osDBs.CreateOpts{Name: "sampledb"},
+ },
+ },
+ },
+ Size: 2,
+ RestorePoint: "1234567890",
+ }
+
+ instance, err := Create(fake.ServiceClient(), opts).Extract()
+
+ th.AssertNoErr(t, err)
+ th.AssertDeepEquals(t, expectedInstance, instance)
+}
+
+func TestGet(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ fixture.SetupHandler(t, resURL, "GET", "", getResp, 200)
+
+ instance, err := Get(fake.ServiceClient(), instanceID).Extract()
+
+ th.AssertNoErr(t, err)
+ th.AssertDeepEquals(t, expectedInstance, instance)
+}
+
+func TestDeleteInstance(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ os.HandleDelete(t)
+
+ res := Delete(fake.ServiceClient(), instanceID)
+ th.AssertNoErr(t, res.Err)
+}
+
+func TestEnableRootUser(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ os.HandleEnableRoot(t)
+
+ expected := &osUsers.User{Name: "root", Password: "secretsecret"}
+
+ user, err := EnableRootUser(fake.ServiceClient(), instanceID).Extract()
+ th.AssertNoErr(t, err)
+ th.AssertDeepEquals(t, expected, user)
+}
+
+func TestRestart(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ os.HandleRestart(t)
+
+ res := Restart(fake.ServiceClient(), instanceID)
+ th.AssertNoErr(t, res.Err)
+}
+
+func TestResize(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ os.HandleResize(t)
+
+ res := Resize(fake.ServiceClient(), instanceID, "2")
+ th.AssertNoErr(t, res.Err)
+}
+
+func TestResizeVolume(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ os.HandleResizeVol(t)
+
+ res := ResizeVolume(fake.ServiceClient(), instanceID, 4)
+ th.AssertNoErr(t, res.Err)
+}
diff --git a/rackspace/db/v1/instances/doc.go b/rackspace/db/v1/instances/doc.go
new file mode 100644
index 0000000..0c8ad63
--- /dev/null
+++ b/rackspace/db/v1/instances/doc.go
@@ -0,0 +1,3 @@
+// Package instances provides information and interaction with the instance API
+// resource in the Rackspace Database service.
+package instances
diff --git a/rackspace/db/v1/instances/fixtures.go b/rackspace/db/v1/instances/fixtures.go
new file mode 100644
index 0000000..c5ff37a
--- /dev/null
+++ b/rackspace/db/v1/instances/fixtures.go
@@ -0,0 +1,340 @@
+package instances
+
+import (
+ "fmt"
+ "time"
+
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/openstack/db/v1/datastores"
+ "github.com/rackspace/gophercloud/openstack/db/v1/flavors"
+ os "github.com/rackspace/gophercloud/openstack/db/v1/instances"
+)
+
+var (
+ timestamp = "2015-11-12T14:22:42Z"
+ timeVal, _ = time.Parse(time.RFC3339, timestamp)
+)
+
+var instance = `
+{
+ "created": "` + timestamp + `",
+ "datastore": {
+ "type": "mysql",
+ "version": "5.6"
+ },
+ "flavor": {
+ "id": "1",
+ "links": [
+ {
+ "href": "https://ord.databases.api.rackspacecloud.com/v1.0/1234/flavors/1",
+ "rel": "self"
+ },
+ {
+ "href": "https://ord.databases.api.rackspacecloud.com/v1.0/1234/flavors/1",
+ "rel": "bookmark"
+ }
+ ]
+ },
+ "links": [
+ {
+ "href": "https://ord.databases.api.rackspacecloud.com/v1.0/1234/flavors/1",
+ "rel": "self"
+ }
+ ],
+ "hostname": "e09ad9a3f73309469cf1f43d11e79549caf9acf2.rackspaceclouddb.com",
+ "id": "{instanceID}",
+ "name": "json_rack_instance",
+ "status": "BUILD",
+ "updated": "` + timestamp + `",
+ "volume": {
+ "size": 2
+ }
+}
+`
+
+var createReq = `
+{
+ "instance": {
+ "databases": [
+ {
+ "character_set": "utf8",
+ "collate": "utf8_general_ci",
+ "name": "sampledb"
+ },
+ {
+ "name": "nextround"
+ }
+ ],
+ "flavorRef": "1",
+ "name": "json_rack_instance",
+ "users": [
+ {
+ "databases": [
+ {
+ "name": "sampledb"
+ }
+ ],
+ "name": "demouser",
+ "password": "demopassword"
+ }
+ ],
+ "volume": {
+ "size": 2
+ },
+ "restorePoint": {
+ "backupRef": "1234567890"
+ }
+ }
+}
+`
+
+var createReplicaReq = `
+{
+ "instance": {
+ "volume": {
+ "size": 1
+ },
+ "flavorRef": "9",
+ "name": "t2s1_ALT_GUEST",
+ "replica_of": "6bdca2fc-418e-40bd-a595-62abda61862d"
+ }
+}
+`
+
+var createReplicaResp = `
+{
+ "instance": {
+ "status": "BUILD",
+ "updated": "` + timestamp + `",
+ "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": "` + timestamp + `",
+ "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"
+ }
+ }
+}
+`
+
+var listReplicasResp = `
+{
+ "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"
+ }
+ }
+ ]
+}
+`
+
+var getReplicaResp = `
+{
+ "instance": {
+ "status": "ACTIVE",
+ "updated": "` + timestamp + `",
+ "name": "t1_ALT_GUEST",
+ "created": "` + timestamp + `",
+ "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"
+ }
+ }
+}
+`
+
+var detachReq = `
+{
+ "instance": {
+ "replica_of": "",
+ "slave_of": ""
+ }
+}
+`
+
+var getConfigResp = `
+{
+ "instance": {
+ "configuration": {
+ "basedir": "/usr",
+ "connect_timeout": "15",
+ "datadir": "/var/lib/mysql",
+ "default_storage_engine": "innodb",
+ "innodb_buffer_pool_instances": "1",
+ "innodb_buffer_pool_size": "175M",
+ "innodb_checksum_algorithm": "crc32",
+ "innodb_data_file_path": "ibdata1:10M:autoextend",
+ "innodb_file_per_table": "1",
+ "innodb_io_capacity": "200",
+ "innodb_log_file_size": "256M",
+ "innodb_log_files_in_group": "2",
+ "innodb_open_files": "8192",
+ "innodb_thread_concurrency": "0",
+ "join_buffer_size": "1M",
+ "key_buffer_size": "50M",
+ "local-infile": "0",
+ "log-error": "/var/log/mysql/mysqld.log",
+ "max_allowed_packet": "16M",
+ "max_connect_errors": "10000",
+ "max_connections": "40",
+ "max_heap_table_size": "16M",
+ "myisam-recover": "BACKUP",
+ "open_files_limit": "8192",
+ "performance_schema": "off",
+ "pid_file": "/var/run/mysqld/mysqld.pid",
+ "port": "3306",
+ "query_cache_limit": "1M",
+ "query_cache_size": "8M",
+ "query_cache_type": "1",
+ "read_buffer_size": "256K",
+ "read_rnd_buffer_size": "1M",
+ "server_id": "1",
+ "skip-external-locking": "1",
+ "skip_name_resolve": "1",
+ "sort_buffer_size": "256K",
+ "table_open_cache": "4096",
+ "thread_stack": "192K",
+ "tmp_table_size": "16M",
+ "tmpdir": "/var/tmp",
+ "user": "mysql",
+ "wait_timeout": "3600"
+ }
+ }
+}
+`
+
+var associateReq = `{"instance": {"configuration": "{configGroupID}"}}`
+
+var listBackupsResp = `
+{
+ "backups": [
+ {
+ "status": "COMPLETED",
+ "updated": "` + timestamp + `",
+ "description": "Backup from Restored Instance",
+ "datastore": {
+ "version": "5.1",
+ "type": "MySQL",
+ "version_id": "20000000-0000-0000-0000-000000000002"
+ },
+ "id": "87972694-4be2-40f5-83f8-501656e0032a",
+ "size": 0.141026,
+ "name": "restored_backup",
+ "created": "` + timestamp + `",
+ "instance_id": "29af2cd9-0674-48ab-b87a-b160f00208e6",
+ "parent_id": null,
+ "locationRef": "http://localhost/path/to/backup"
+ }
+ ]
+}
+`
+
+var (
+ createResp = fmt.Sprintf(`{"instance":%s}`, instance)
+ getResp = fmt.Sprintf(`{"instance":%s}`, instance)
+ associateResp = fmt.Sprintf(`{"instance":%s}`, instance)
+ listInstancesResp = fmt.Sprintf(`{"instances":[%s]}`, instance)
+)
+
+var instanceID = "{instanceID}"
+
+var expectedInstance = &Instance{
+ Created: timeVal,
+ Updated: timeVal,
+ Datastore: datastores.DatastorePartial{Type: "mysql", Version: "5.6"},
+ Flavor: flavors.Flavor{
+ ID: "1",
+ Links: []gophercloud.Link{
+ gophercloud.Link{Href: "https://ord.databases.api.rackspacecloud.com/v1.0/1234/flavors/1", Rel: "self"},
+ gophercloud.Link{Href: "https://ord.databases.api.rackspacecloud.com/v1.0/1234/flavors/1", Rel: "bookmark"},
+ },
+ },
+ Hostname: "e09ad9a3f73309469cf1f43d11e79549caf9acf2.rackspaceclouddb.com",
+ ID: instanceID,
+ Links: []gophercloud.Link{
+ gophercloud.Link{Href: "https://ord.databases.api.rackspacecloud.com/v1.0/1234/flavors/1", Rel: "self"},
+ },
+ Name: "json_rack_instance",
+ Status: "BUILD",
+ Volume: os.Volume{Size: 2},
+}
+
+var expectedReplica = &Instance{
+ Status: "BUILD",
+ Updated: timeVal,
+ 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: timeVal,
+ ID: "8367c312-7c40-4a66-aab1-5767478914fc",
+ Volume: os.Volume{Size: 1},
+ Flavor: flavors.Flavor{ID: "9"},
+ Datastore: datastores.DatastorePartial{Version: "5.6", Type: "mysql"},
+ ReplicaOf: &Instance{
+ ID: "6bdca2fc-418e-40bd-a595-62abda61862d",
+ },
+}
diff --git a/rackspace/db/v1/instances/requests.go b/rackspace/db/v1/instances/requests.go
new file mode 100644
index 0000000..f4df692
--- /dev/null
+++ b/rackspace/db/v1/instances/requests.go
@@ -0,0 +1,199 @@
+package instances
+
+import (
+ "github.com/rackspace/gophercloud"
+ osDBs "github.com/rackspace/gophercloud/openstack/db/v1/databases"
+ os "github.com/rackspace/gophercloud/openstack/db/v1/instances"
+ osUsers "github.com/rackspace/gophercloud/openstack/db/v1/users"
+ "github.com/rackspace/gophercloud/pagination"
+ "github.com/rackspace/gophercloud/rackspace/db/v1/backups"
+)
+
+// CreateOpts is the struct responsible for configuring a new database instance.
+type CreateOpts struct {
+ // Either the integer UUID (in string form) of the flavor, or its URI
+ // reference as specified in the response from the List() call. Required.
+ FlavorRef string
+
+ // Specifies the volume size in gigabytes (GB). The value must be between 1
+ // and 300. Required.
+ Size int
+
+ // Name of the instance to create. The length of the name is limited to
+ // 255 characters and any characters are permitted. Optional.
+ Name string
+
+ // A slice of database information options.
+ Databases osDBs.CreateOptsBuilder
+
+ // A slice of user information options.
+ Users osUsers.CreateOptsBuilder
+
+ // ID of the configuration group to associate with the instance. Optional.
+ ConfigID string
+
+ // Options to configure the type of datastore the instance will use. This is
+ // optional, and if excluded will default to MySQL.
+ Datastore *os.DatastoreOpts
+
+ // Specifies the backup ID from which to restore the database instance. There
+ // are some things to be aware of before using this field. When you execute
+ // the Restore Backup operation, a new database instance is created to store
+ // the backup whose ID is specified by the restorePoint attribute. This will
+ // mean that:
+ // - All users, passwords and access that were on the instance at the time of
+ // the backup will be restored along with the databases.
+ // - 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) {
+ instance, err := os.CreateOpts{
+ FlavorRef: opts.FlavorRef,
+ Size: opts.Size,
+ Name: opts.Name,
+ Databases: opts.Databases,
+ Users: opts.Users,
+ }.ToInstanceCreateMap()
+
+ if err != nil {
+ return nil, err
+ }
+
+ instance = instance["instance"].(map[string]interface{})
+
+ if opts.ConfigID != "" {
+ instance["configuration"] = opts.ConfigID
+ }
+
+ if opts.Datastore != nil {
+ ds, err := opts.Datastore.ToMap()
+ if err != nil {
+ return nil, err
+ }
+ instance["datastore"] = ds
+ }
+
+ if opts.RestorePoint != "" {
+ instance["restorePoint"] = map[string]string{"backupRef": opts.RestorePoint}
+ }
+
+ if opts.ReplicaOf != "" {
+ instance["replica_of"] = opts.ReplicaOf
+ }
+
+ return map[string]interface{}{"instance": instance}, nil
+}
+
+// Create asynchronously provisions a new database instance. It requires the
+// user to specify a flavor and a volume size. The API service then provisions
+// the instance with the requested flavor and sets up a volume of the specified
+// size, which is the storage for the database instance.
+//
+// Although this call only allows the creation of 1 instance per request, you
+// can create an instance with multiple databases and users. The default
+// binding for a MySQL instance is port 3306.
+func Create(client *gophercloud.ServiceClient, opts os.CreateOptsBuilder) CreateResult {
+ return CreateResult{os.Create(client, opts)}
+}
+
+// ListOpts specifies all of the query options to be used when returning a list
+// of database instances.
+type ListOpts struct {
+ // IncludeHA includes or excludes High Availability instances from the result set
+ IncludeHA bool `q:"include_ha"`
+
+ // IncludeReplicas includes or excludes Replica instances from the result set
+ IncludeReplicas bool `q:"include_replicas"`
+}
+
+// ToInstanceListQuery formats a ListOpts into a query string.
+func (opts ListOpts) ToInstanceListQuery() (string, error) {
+ q, err := gophercloud.BuildQueryString(opts)
+ if err != nil {
+ return "", err
+ }
+ return q.String(), nil
+}
+
+// List retrieves the status and information for all database instances.
+func List(client *gophercloud.ServiceClient, opts *ListOpts) pagination.Pager {
+ url := baseURL(client)
+
+ if opts != nil {
+ query, err := opts.ToInstanceListQuery()
+ if err != nil {
+ return pagination.Pager{Err: err}
+ }
+ url += query
+ }
+
+ createPageFn := func(r pagination.PageResult) pagination.Page {
+ return os.InstancePage{pagination.LinkedPageBase{PageResult: r}}
+ }
+
+ return pagination.NewPager(client, url, createPageFn)
+}
+
+// GetDefaultConfig lists the default configuration settings from the template
+// that was applied to the specified instance. In a sense, this is the vanilla
+// configuration setting applied to an instance. Further configuration can be
+// applied by associating an instance with a configuration group.
+func GetDefaultConfig(client *gophercloud.ServiceClient, id string) ConfigResult {
+ var res ConfigResult
+
+ _, res.Err = client.Request("GET", configURL(client, id), gophercloud.RequestOpts{
+ JSONResponse: &res.Body,
+ OkCodes: []int{200},
+ })
+
+ return res
+}
+
+// AssociateWithConfigGroup associates a specified instance to a specified
+// configuration group. If any of the parameters within a configuration group
+// require a restart, then the instance will transition into a restart.
+func AssociateWithConfigGroup(client *gophercloud.ServiceClient, instanceID, configGroupID string) UpdateResult {
+ reqBody := map[string]string{
+ "configuration": configGroupID,
+ }
+
+ var res UpdateResult
+
+ _, res.Err = client.Request("PUT", resourceURL(client, instanceID), gophercloud.RequestOpts{
+ JSONBody: map[string]map[string]string{"instance": reqBody},
+ OkCodes: []int{202},
+ })
+
+ return res
+}
+
+// DetachFromConfigGroup will detach an instance from all config groups.
+func DetachFromConfigGroup(client *gophercloud.ServiceClient, instanceID string) UpdateResult {
+ return AssociateWithConfigGroup(client, instanceID, "")
+}
+
+// ListBackups will list all the backups for a specified database instance.
+func ListBackups(client *gophercloud.ServiceClient, instanceID string) pagination.Pager {
+ pageFn := func(r pagination.PageResult) pagination.Page {
+ return backups.BackupPage{pagination.SinglePageBase(r)}
+ }
+ return pagination.NewPager(client, backupsURL(client, instanceID), pageFn)
+}
+
+// DetachReplica will detach a specified replica instance from its source
+// instance, effectively allowing it to operate independently. Detaching a
+// replica will restart the MySQL service on the instance.
+func DetachReplica(client *gophercloud.ServiceClient, replicaID string) DetachResult {
+ var res DetachResult
+
+ _, res.Err = client.Request("PATCH", resourceURL(client, replicaID), gophercloud.RequestOpts{
+ JSONBody: 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
new file mode 100644
index 0000000..7fa4601
--- /dev/null
+++ b/rackspace/db/v1/instances/requests_test.go
@@ -0,0 +1,246 @@
+package instances
+
+import (
+ "testing"
+
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/openstack/db/v1/datastores"
+ "github.com/rackspace/gophercloud/openstack/db/v1/flavors"
+ 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"
+ "github.com/rackspace/gophercloud/testhelper/fixture"
+)
+
+func TestInstanceList(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ fixture.SetupHandler(t, "/instances", "GET", "", listInstancesResp, 200)
+
+ opts := &ListOpts{
+ IncludeHA: false,
+ IncludeReplicas: false,
+ }
+
+ pages := 0
+ err := List(fake.ServiceClient(), opts).EachPage(func(page pagination.Page) (bool, error) {
+ pages++
+
+ actual, err := ExtractInstances(page)
+ if err != nil {
+ return false, err
+ }
+
+ th.CheckDeepEquals(t, []Instance{*expectedInstance}, actual)
+ return true, nil
+ })
+
+ th.AssertNoErr(t, err)
+ th.AssertEquals(t, 1, pages)
+}
+
+func TestGetConfig(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ fixture.SetupHandler(t, resURL+"/configuration", "GET", "", getConfigResp, 200)
+
+ config, err := GetDefaultConfig(fake.ServiceClient(), instanceID).Extract()
+
+ expected := map[string]string{
+ "basedir": "/usr",
+ "connect_timeout": "15",
+ "datadir": "/var/lib/mysql",
+ "default_storage_engine": "innodb",
+ "innodb_buffer_pool_instances": "1",
+ "innodb_buffer_pool_size": "175M",
+ "innodb_checksum_algorithm": "crc32",
+ "innodb_data_file_path": "ibdata1:10M:autoextend",
+ "innodb_file_per_table": "1",
+ "innodb_io_capacity": "200",
+ "innodb_log_file_size": "256M",
+ "innodb_log_files_in_group": "2",
+ "innodb_open_files": "8192",
+ "innodb_thread_concurrency": "0",
+ "join_buffer_size": "1M",
+ "key_buffer_size": "50M",
+ "local-infile": "0",
+ "log-error": "/var/log/mysql/mysqld.log",
+ "max_allowed_packet": "16M",
+ "max_connect_errors": "10000",
+ "max_connections": "40",
+ "max_heap_table_size": "16M",
+ "myisam-recover": "BACKUP",
+ "open_files_limit": "8192",
+ "performance_schema": "off",
+ "pid_file": "/var/run/mysqld/mysqld.pid",
+ "port": "3306",
+ "query_cache_limit": "1M",
+ "query_cache_size": "8M",
+ "query_cache_type": "1",
+ "read_buffer_size": "256K",
+ "read_rnd_buffer_size": "1M",
+ "server_id": "1",
+ "skip-external-locking": "1",
+ "skip_name_resolve": "1",
+ "sort_buffer_size": "256K",
+ "table_open_cache": "4096",
+ "thread_stack": "192K",
+ "tmp_table_size": "16M",
+ "tmpdir": "/var/tmp",
+ "user": "mysql",
+ "wait_timeout": "3600",
+ }
+
+ th.AssertNoErr(t, err)
+ th.AssertDeepEquals(t, expected, config)
+}
+
+func TestAssociateWithConfigGroup(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ fixture.SetupHandler(t, resURL, "PUT", associateReq, associateResp, 202)
+
+ res := AssociateWithConfigGroup(fake.ServiceClient(), instanceID, "{configGroupID}")
+ th.AssertNoErr(t, res.Err)
+}
+
+func TestListBackups(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ fixture.SetupHandler(t, resURL+"/backups", "GET", "", listBackupsResp, 200)
+
+ pages := 0
+
+ err := ListBackups(fake.ServiceClient(), instanceID).EachPage(func(page pagination.Page) (bool, error) {
+ pages++
+ actual, err := backups.ExtractBackups(page)
+ th.AssertNoErr(t, err)
+
+ expected := []backups.Backup{
+ backups.Backup{
+ Created: timeVal,
+ Description: "Backup from Restored Instance",
+ ID: "87972694-4be2-40f5-83f8-501656e0032a",
+ InstanceID: "29af2cd9-0674-48ab-b87a-b160f00208e6",
+ LocationRef: "http://localhost/path/to/backup",
+ Name: "restored_backup",
+ ParentID: "",
+ Size: 0.141026,
+ Status: "COMPLETED",
+ Updated: timeVal,
+ Datastore: datastores.DatastorePartial{Version: "5.1", Type: "MySQL", VersionID: "20000000-0000-0000-0000-000000000002"},
+ },
+ }
+
+ th.AssertDeepEquals(t, expected, actual)
+ return true, nil
+ })
+
+ th.AssertNoErr(t, err)
+ th.AssertEquals(t, 1, pages)
+}
+
+func TestCreateReplica(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ fixture.SetupHandler(t, _rootURL, "POST", createReplicaReq, createReplicaResp, 200)
+
+ 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()
+ fixture.SetupHandler(t, _rootURL, "GET", "", listReplicasResp, 200)
+
+ pages := 0
+ err := List(fake.ServiceClient(), nil).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: flavors.Flavor{ID: "9"},
+ Datastore: datastores.DatastorePartial{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)
+ th.AssertEquals(t, 1, pages)
+}
+
+func TestGetReplica(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ fixture.SetupHandler(t, resURL, "GET", "", getReplicaResp, 200)
+
+ replica, err := Get(fake.ServiceClient(), instanceID).Extract()
+ th.AssertNoErr(t, err)
+
+ expectedReplica := &Instance{
+ Status: "ACTIVE",
+ Updated: timeVal,
+ Name: "t1_ALT_GUEST",
+ Created: timeVal,
+ 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: flavors.Flavor{ID: "9"},
+ Datastore: datastores.DatastorePartial{
+ Version: "5.6",
+ Type: "mysql",
+ },
+ }
+
+ th.AssertDeepEquals(t, replica, expectedReplica)
+}
+
+func TestDetachReplica(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ fixture.SetupHandler(t, resURL, "PATCH", detachReq, "", 202)
+
+ err := DetachReplica(fake.ServiceClient(), instanceID).ExtractErr()
+ th.AssertNoErr(t, err)
+}
diff --git a/rackspace/db/v1/instances/results.go b/rackspace/db/v1/instances/results.go
new file mode 100644
index 0000000..4b1317e
--- /dev/null
+++ b/rackspace/db/v1/instances/results.go
@@ -0,0 +1,183 @@
+package instances
+
+import (
+ "time"
+
+ "github.com/mitchellh/mapstructure"
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/openstack/db/v1/datastores"
+ "github.com/rackspace/gophercloud/openstack/db/v1/flavors"
+ os "github.com/rackspace/gophercloud/openstack/db/v1/instances"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+// Instance represents a remote MySQL instance.
+type Instance struct {
+ // Indicates the datetime that the instance was created
+ Created time.Time `mapstructure:"-"`
+
+ // Indicates the most recent datetime that the instance was updated.
+ Updated time.Time `mapstructure:"-"`
+
+ // Indicates how the instance stores data.
+ Datastore datastores.DatastorePartial
+
+ // Indicates the hardware flavor the instance uses.
+ Flavor flavors.Flavor
+
+ // A DNS-resolvable hostname associated with the database instance (rather
+ // than an IPv4 address). Since the hostname always resolves to the correct
+ // IP address of the database instance, this relieves the user from the task
+ // of maintaining the mapping. Note that although the IP address may likely
+ // change on resizing, migrating, and so forth, the hostname always resolves
+ // to the correct database instance.
+ Hostname string
+
+ // Indicates the unique identifier for the instance resource.
+ ID string
+
+ // Exposes various links that reference the instance resource.
+ Links []gophercloud.Link
+
+ // The human-readable name of the instance.
+ Name string
+
+ // The build status of the instance.
+ Status string
+
+ // Information about the attached volume of the instance.
+ Volume os.Volume
+
+ // IP indicates the various IP addresses which allow access.
+ IP []string
+
+ // Indicates whether this instance is a replica of another source instance.
+ ReplicaOf *Instance `mapstructure:"replica_of" json:"replica_of"`
+
+ // Indicates whether this instance is the source of other replica instances.
+ Replicas []Instance
+}
+
+func commonExtract(err error, body interface{}) (*Instance, error) {
+ if err != nil {
+ return nil, err
+ }
+
+ var response struct {
+ Instance Instance `mapstructure:"instance"`
+ }
+
+ err = mapstructure.Decode(body, &response)
+
+ val := body.(map[string]interface{})["instance"].(map[string]interface{})
+
+ if t, ok := val["created"].(string); ok && t != "" {
+ creationTime, err := time.Parse(time.RFC3339, t)
+ if err != nil {
+ return &response.Instance, err
+ }
+ response.Instance.Created = creationTime
+ }
+
+ if t, ok := val["updated"].(string); ok && t != "" {
+ updatedTime, err := time.Parse(time.RFC3339, t)
+ if err != nil {
+ return &response.Instance, err
+ }
+ response.Instance.Updated = updatedTime
+ }
+
+ return &response.Instance, err
+}
+
+// CreateResult represents the result of a Create operation.
+type CreateResult struct {
+ os.CreateResult
+}
+
+// Extract will retrieve an instance from a create result.
+func (r CreateResult) Extract() (*Instance, error) {
+ return commonExtract(r.Err, r.Body)
+}
+
+// GetResult represents the result of a Get operation.
+type GetResult struct {
+ os.GetResult
+}
+
+// Extract will extract an Instance from a GetResult.
+func (r GetResult) Extract() (*Instance, error) {
+ return commonExtract(r.Err, r.Body)
+}
+
+// ConfigResult represents the result of getting default configuration for an
+// instance.
+type ConfigResult struct {
+ gophercloud.Result
+}
+
+// DetachResult represents the result of detaching a replica from its source.
+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) {
+ if r.Err != nil {
+ return nil, r.Err
+ }
+
+ var response struct {
+ Instance struct {
+ Config map[string]string `mapstructure:"configuration"`
+ } `mapstructure:"instance"`
+ }
+
+ err := mapstructure.Decode(r.Body, &response)
+ return response.Instance.Config, err
+}
+
+// UpdateResult represents the result of an Update operation.
+type UpdateResult struct {
+ gophercloud.ErrResult
+}
+
+// ExtractInstances retrieves a slice of instances from a paginated collection.
+func ExtractInstances(page pagination.Page) ([]Instance, error) {
+ casted := page.(os.InstancePage).Body
+
+ var response struct {
+ Instances []Instance `mapstructure:"instances"`
+ }
+
+ err := mapstructure.Decode(casted, &response)
+
+ var vals []interface{}
+ switch (casted).(type) {
+ case interface{}:
+ vals = casted.(map[string]interface{})["instances"].([]interface{})
+ }
+
+ for i, v := range vals {
+ val := v.(map[string]interface{})
+
+ if t, ok := val["created"].(string); ok && t != "" {
+ creationTime, err := time.Parse(time.RFC3339, t)
+ if err != nil {
+ return response.Instances, err
+ }
+ response.Instances[i].Created = creationTime
+ }
+
+ if t, ok := val["updated"].(string); ok && t != "" {
+ updatedTime, err := time.Parse(time.RFC3339, t)
+ if err != nil {
+ return response.Instances, err
+ }
+ response.Instances[i].Updated = updatedTime
+ }
+ }
+
+ return response.Instances, err
+}
diff --git a/rackspace/db/v1/instances/urls.go b/rackspace/db/v1/instances/urls.go
new file mode 100644
index 0000000..5955f4c
--- /dev/null
+++ b/rackspace/db/v1/instances/urls.go
@@ -0,0 +1,23 @@
+package instances
+
+import "github.com/rackspace/gophercloud"
+
+func baseURL(c *gophercloud.ServiceClient) string {
+ return c.ServiceURL("instances")
+}
+
+func createURL(c *gophercloud.ServiceClient) string {
+ return baseURL(c)
+}
+
+func resourceURL(c *gophercloud.ServiceClient, id string) string {
+ return c.ServiceURL("instances", id)
+}
+
+func configURL(c *gophercloud.ServiceClient, id string) string {
+ return c.ServiceURL("instances", id, "configuration")
+}
+
+func backupsURL(c *gophercloud.ServiceClient, id string) string {
+ return c.ServiceURL("instances", id, "backups")
+}
diff --git a/rackspace/db/v1/users/delegate.go b/rackspace/db/v1/users/delegate.go
new file mode 100644
index 0000000..8298c46
--- /dev/null
+++ b/rackspace/db/v1/users/delegate.go
@@ -0,0 +1,16 @@
+package users
+
+import (
+ "github.com/rackspace/gophercloud"
+ os "github.com/rackspace/gophercloud/openstack/db/v1/users"
+)
+
+// Create will create a new database user for the specified database instance.
+func Create(client *gophercloud.ServiceClient, instanceID string, opts os.CreateOptsBuilder) os.CreateResult {
+ return os.Create(client, instanceID, opts)
+}
+
+// Delete will permanently remove a user from a specified database instance.
+func Delete(client *gophercloud.ServiceClient, instanceID, userName string) os.DeleteResult {
+ return os.Delete(client, instanceID, userName)
+}
diff --git a/rackspace/db/v1/users/delegate_test.go b/rackspace/db/v1/users/delegate_test.go
new file mode 100644
index 0000000..7a1b773
--- /dev/null
+++ b/rackspace/db/v1/users/delegate_test.go
@@ -0,0 +1,48 @@
+package users
+
+import (
+ "testing"
+
+ db "github.com/rackspace/gophercloud/openstack/db/v1/databases"
+ os "github.com/rackspace/gophercloud/openstack/db/v1/users"
+ th "github.com/rackspace/gophercloud/testhelper"
+ fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+const instanceID = "{instanceID}"
+
+func TestCreate(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ os.HandleCreate(t)
+
+ opts := os.BatchCreateOpts{
+ os.CreateOpts{
+ Databases: db.BatchCreateOpts{
+ db.CreateOpts{Name: "databaseA"},
+ },
+ Name: "dbuser3",
+ Password: "secretsecret",
+ },
+ os.CreateOpts{
+ Databases: db.BatchCreateOpts{
+ db.CreateOpts{Name: "databaseB"},
+ db.CreateOpts{Name: "databaseC"},
+ },
+ Name: "dbuser4",
+ Password: "secretsecret",
+ },
+ }
+
+ res := Create(fake.ServiceClient(), instanceID, opts)
+ th.AssertNoErr(t, res.Err)
+}
+
+func TestDelete(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ os.HandleDelete(t)
+
+ res := Delete(fake.ServiceClient(), instanceID, "{userName}")
+ th.AssertNoErr(t, res.Err)
+}
diff --git a/rackspace/db/v1/users/doc.go b/rackspace/db/v1/users/doc.go
new file mode 100644
index 0000000..84f2eb3
--- /dev/null
+++ b/rackspace/db/v1/users/doc.go
@@ -0,0 +1,3 @@
+// Package users provides information and interaction with the user API
+// resource in the Rackspace Database service.
+package users
diff --git a/rackspace/db/v1/users/fixtures.go b/rackspace/db/v1/users/fixtures.go
new file mode 100644
index 0000000..5314e85
--- /dev/null
+++ b/rackspace/db/v1/users/fixtures.go
@@ -0,0 +1,77 @@
+package users
+
+const singleDB = `{"databases": [{"name": "databaseE"}]}`
+
+var changePwdReq = `
+{
+ "users": [
+ {
+ "name": "dbuser1",
+ "password": "newpassword"
+ },
+ {
+ "name": "dbuser2",
+ "password": "anotherpassword"
+ }
+ ]
+}
+`
+
+var updateReq = `
+{
+ "user": {
+ "name": "new_username",
+ "password": "new_password"
+ }
+}
+`
+
+var getResp = `
+{
+ "user": {
+ "name": "exampleuser",
+ "host": "foo",
+ "databases": [
+ {
+ "name": "databaseA"
+ },
+ {
+ "name": "databaseB"
+ }
+ ]
+ }
+}
+`
+
+var listResp = `
+{
+"users": [
+ {
+ "name": "dbuser1",
+ "host": "localhost",
+ "databases": [
+ {
+ "name": "databaseA"
+ }
+ ]
+ },
+ {
+ "name": "dbuser2",
+ "host": "localhost",
+ "databases": [
+ {
+ "name": "databaseB"
+ },
+ {
+ "name": "databaseC"
+ }
+ ]
+ }
+]
+}
+`
+
+var (
+ listUserAccessResp = singleDB
+ grantUserAccessReq = singleDB
+)
diff --git a/rackspace/db/v1/users/requests.go b/rackspace/db/v1/users/requests.go
new file mode 100644
index 0000000..74e47ab
--- /dev/null
+++ b/rackspace/db/v1/users/requests.go
@@ -0,0 +1,176 @@
+package users
+
+import (
+ "errors"
+
+ "github.com/rackspace/gophercloud"
+ db "github.com/rackspace/gophercloud/openstack/db/v1/databases"
+ os "github.com/rackspace/gophercloud/openstack/db/v1/users"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+// List will list all available users for a specified database instance.
+func List(client *gophercloud.ServiceClient, instanceID string) pagination.Pager {
+ createPageFn := func(r pagination.PageResult) pagination.Page {
+ return UserPage{pagination.LinkedPageBase{PageResult: r}}
+ }
+
+ return pagination.NewPager(client, baseURL(client, instanceID), createPageFn)
+}
+
+/*
+ChangePassword changes the password for one or more users. For example, to
+change the respective passwords for two users:
+
+ opts := os.BatchCreateOpts{
+ os.CreateOpts{Name: "db_user_1", Password: "new_password_1"},
+ os.CreateOpts{Name: "db_user_2", Password: "new_password_2"},
+ }
+
+ ChangePassword(client, "instance_id", opts)
+*/
+func ChangePassword(client *gophercloud.ServiceClient, instanceID string, opts os.CreateOptsBuilder) UpdatePasswordsResult {
+ var res UpdatePasswordsResult
+
+ reqBody, err := opts.ToUserCreateMap()
+ if err != nil {
+ res.Err = err
+ return res
+ }
+
+ _, res.Err = client.Request("PUT", baseURL(client, instanceID), gophercloud.RequestOpts{
+ JSONBody: &reqBody,
+ OkCodes: []int{202},
+ })
+
+ return res
+}
+
+// UpdateOpts is the struct responsible for updating an existing user.
+type UpdateOpts struct {
+ // [OPTIONAL] Specifies a name for the user. Valid names can be composed
+ // of the following characters: letters (either case); numbers; these
+ // characters '@', '?', '#', ' ' but NEVER beginning a name string; '_' is
+ // permitted anywhere. Prohibited characters that are forbidden include:
+ // single quotes, double quotes, back quotes, semicolons, commas, backslashes,
+ // and forward slashes. Spaces at the front or end of a user name are also
+ // not permitted.
+ Name string
+
+ // [OPTIONAL] Specifies a password for the user.
+ Password string
+
+ // [OPTIONAL] Specifies the host from which a user is allowed to connect to
+ // the database. Possible values are a string containing an IPv4 address or
+ // "%" to allow connecting from any host. Optional; the default is "%".
+ Host string
+}
+
+// ToMap is a convenience function for creating sub-maps for individual users.
+func (opts UpdateOpts) ToMap() (map[string]interface{}, error) {
+ if opts.Name == "root" {
+ return nil, errors.New("root is a reserved user name and cannot be used")
+ }
+
+ user := map[string]interface{}{}
+
+ if opts.Name != "" {
+ user["name"] = opts.Name
+ }
+
+ if opts.Password != "" {
+ user["password"] = opts.Password
+ }
+
+ if opts.Host != "" {
+ user["host"] = opts.Host
+ }
+
+ return user, nil
+}
+
+// Update will modify the attributes of a specified user. Attributes that can
+// be updated are: user name, password, and host.
+func Update(client *gophercloud.ServiceClient, instanceID, userName string, opts UpdateOpts) UpdateResult {
+ var res UpdateResult
+
+ reqBody, err := opts.ToMap()
+ if err != nil {
+ res.Err = err
+ return res
+ }
+ reqBody = map[string]interface{}{"user": reqBody}
+
+ _, res.Err = client.Request("PUT", userURL(client, instanceID, userName), gophercloud.RequestOpts{
+ JSONBody: &reqBody,
+ OkCodes: []int{202},
+ })
+
+ return res
+}
+
+// Get will retrieve the details for a particular user.
+func Get(client *gophercloud.ServiceClient, instanceID, userName string) GetResult {
+ var res GetResult
+
+ _, res.Err = client.Request("GET", userURL(client, instanceID, userName), gophercloud.RequestOpts{
+ JSONResponse: &res.Body,
+ OkCodes: []int{200},
+ })
+
+ return res
+}
+
+// ListAccess will list all of the databases a user has access to.
+func ListAccess(client *gophercloud.ServiceClient, instanceID, userName string) pagination.Pager {
+ pageFn := func(r pagination.PageResult) pagination.Page {
+ return AccessPage{pagination.LinkedPageBase{PageResult: r}}
+ }
+
+ return pagination.NewPager(client, dbsURL(client, instanceID, userName), pageFn)
+}
+
+/*
+GrantAccess for the specified user to one or more databases on a specified
+instance. For example, to add a user to multiple databases:
+
+ opts := db.BatchCreateOpts{
+ db.CreateOpts{Name: "database_1"},
+ db.CreateOpts{Name: "database_3"},
+ db.CreateOpts{Name: "database_19"},
+ }
+
+ GrantAccess(client, "instance_id", "user_name", opts)
+*/
+func GrantAccess(client *gophercloud.ServiceClient, instanceID, userName string, opts db.CreateOptsBuilder) GrantAccessResult {
+ var res GrantAccessResult
+
+ reqBody, err := opts.ToDBCreateMap()
+ if err != nil {
+ res.Err = err
+ return res
+ }
+
+ _, res.Err = client.Request("PUT", dbsURL(client, instanceID, userName), gophercloud.RequestOpts{
+ JSONBody: &reqBody,
+ OkCodes: []int{202},
+ })
+
+ return res
+}
+
+/*
+RevokeAccess will revoke access for the specified user to one or more databases
+on a specified instance. For example:
+
+ RevokeAccess(client, "instance_id", "user_name", "db_name")
+*/
+func RevokeAccess(client *gophercloud.ServiceClient, instanceID, userName, dbName string) RevokeAccessResult {
+ var res RevokeAccessResult
+
+ _, res.Err = client.Request("DELETE", dbURL(client, instanceID, userName, dbName), gophercloud.RequestOpts{
+ OkCodes: []int{202},
+ })
+
+ return res
+}
diff --git a/rackspace/db/v1/users/requests_test.go b/rackspace/db/v1/users/requests_test.go
new file mode 100644
index 0000000..2f2dca7
--- /dev/null
+++ b/rackspace/db/v1/users/requests_test.go
@@ -0,0 +1,156 @@
+package users
+
+import (
+ "testing"
+
+ db "github.com/rackspace/gophercloud/openstack/db/v1/databases"
+ os "github.com/rackspace/gophercloud/openstack/db/v1/users"
+ "github.com/rackspace/gophercloud/pagination"
+ th "github.com/rackspace/gophercloud/testhelper"
+ fake "github.com/rackspace/gophercloud/testhelper/client"
+ "github.com/rackspace/gophercloud/testhelper/fixture"
+)
+
+var (
+ userName = "{userName}"
+ _rootURL = "/instances/" + instanceID + "/users"
+ _userURL = _rootURL + "/" + userName
+ _dbURL = _userURL + "/databases"
+)
+
+func TestChangeUserPassword(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ fixture.SetupHandler(t, _rootURL, "PUT", changePwdReq, "", 202)
+
+ opts := os.BatchCreateOpts{
+ os.CreateOpts{Name: "dbuser1", Password: "newpassword"},
+ os.CreateOpts{Name: "dbuser2", Password: "anotherpassword"},
+ }
+
+ err := ChangePassword(fake.ServiceClient(), instanceID, opts).ExtractErr()
+ th.AssertNoErr(t, err)
+}
+
+func TestUpdateUser(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ fixture.SetupHandler(t, _userURL, "PUT", updateReq, "", 202)
+
+ opts := UpdateOpts{
+ Name: "new_username",
+ Password: "new_password",
+ }
+
+ err := Update(fake.ServiceClient(), instanceID, userName, opts).ExtractErr()
+ th.AssertNoErr(t, err)
+}
+
+func TestGetUser(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ fixture.SetupHandler(t, _userURL, "GET", "", getResp, 200)
+
+ user, err := Get(fake.ServiceClient(), instanceID, userName).Extract()
+
+ th.AssertNoErr(t, err)
+
+ expected := &User{
+ Name: "exampleuser",
+ Host: "foo",
+ Databases: []db.Database{
+ db.Database{Name: "databaseA"},
+ db.Database{Name: "databaseB"},
+ },
+ }
+
+ th.AssertDeepEquals(t, expected, user)
+}
+
+func TestUserAccessList(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ fixture.SetupHandler(t, _userURL+"/databases", "GET", "", listUserAccessResp, 200)
+
+ expectedDBs := []db.Database{
+ db.Database{Name: "databaseE"},
+ }
+
+ pages := 0
+ err := ListAccess(fake.ServiceClient(), instanceID, userName).EachPage(func(page pagination.Page) (bool, error) {
+ pages++
+
+ actual, err := ExtractDBs(page)
+ if err != nil {
+ return false, err
+ }
+
+ th.CheckDeepEquals(t, expectedDBs, actual)
+
+ return true, nil
+ })
+
+ th.AssertNoErr(t, err)
+ th.AssertEquals(t, 1, pages)
+}
+
+func TestUserList(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ fixture.SetupHandler(t, "/instances/"+instanceID+"/users", "GET", "", listResp, 200)
+
+ expectedUsers := []User{
+ User{
+ Databases: []db.Database{
+ db.Database{Name: "databaseA"},
+ },
+ Name: "dbuser1",
+ Host: "localhost",
+ },
+ User{
+ Databases: []db.Database{
+ db.Database{Name: "databaseB"},
+ db.Database{Name: "databaseC"},
+ },
+ Name: "dbuser2",
+ Host: "localhost",
+ },
+ }
+
+ pages := 0
+ err := List(fake.ServiceClient(), instanceID).EachPage(func(page pagination.Page) (bool, error) {
+ pages++
+
+ actual, err := ExtractUsers(page)
+ if err != nil {
+ return false, err
+ }
+
+ th.CheckDeepEquals(t, expectedUsers, actual)
+
+ return true, nil
+ })
+
+ th.AssertNoErr(t, err)
+ th.AssertEquals(t, 1, pages)
+}
+
+func TestGrantAccess(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ fixture.SetupHandler(t, _dbURL, "PUT", grantUserAccessReq, "", 202)
+
+ opts := db.BatchCreateOpts{db.CreateOpts{Name: "databaseE"}}
+ err := GrantAccess(fake.ServiceClient(), instanceID, userName, opts).ExtractErr()
+ th.AssertNoErr(t, err)
+}
+
+func TestRevokeAccess(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ fixture.SetupHandler(t, _dbURL+"/{dbName}", "DELETE", "", "", 202)
+
+ err := RevokeAccess(fake.ServiceClient(), instanceID, userName, "{dbName}").ExtractErr()
+ th.AssertNoErr(t, err)
+}
diff --git a/rackspace/db/v1/users/results.go b/rackspace/db/v1/users/results.go
new file mode 100644
index 0000000..85b3a7a
--- /dev/null
+++ b/rackspace/db/v1/users/results.go
@@ -0,0 +1,149 @@
+package users
+
+import (
+ "github.com/mitchellh/mapstructure"
+ "github.com/rackspace/gophercloud"
+ db "github.com/rackspace/gophercloud/openstack/db/v1/databases"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+// User represents a database user
+type User struct {
+ // The user name
+ Name string
+
+ // The user password
+ Password string
+
+ // Specifies the host from which a user is allowed to connect to the database.
+ // Possible values are a string containing an IPv4 address or "%" to allow
+ // connecting from any host.
+ Host string
+
+ // The databases associated with this user
+ Databases []db.Database
+}
+
+// UpdatePasswordsResult represents the result of changing a user password.
+type UpdatePasswordsResult struct {
+ gophercloud.ErrResult
+}
+
+// UpdateResult represents the result of updating a user.
+type UpdateResult struct {
+ gophercloud.ErrResult
+}
+
+// GetResult represents the result of getting a user.
+type GetResult struct {
+ gophercloud.Result
+}
+
+// Extract will retrieve a User struct from a getresult.
+func (r GetResult) Extract() (*User, error) {
+ if r.Err != nil {
+ return nil, r.Err
+ }
+
+ var response struct {
+ User User `mapstructure:"user"`
+ }
+
+ err := mapstructure.Decode(r.Body, &response)
+ return &response.User, err
+}
+
+// AccessPage represents a single page of a paginated user collection.
+type AccessPage struct {
+ pagination.LinkedPageBase
+}
+
+// IsEmpty checks to see whether the collection is empty.
+func (page AccessPage) IsEmpty() (bool, error) {
+ users, err := ExtractDBs(page)
+ if err != nil {
+ return true, err
+ }
+ return len(users) == 0, nil
+}
+
+// NextPageURL will retrieve the next page URL.
+func (page AccessPage) NextPageURL() (string, error) {
+ type resp struct {
+ Links []gophercloud.Link `mapstructure:"databases_links"`
+ }
+
+ var r resp
+ err := mapstructure.Decode(page.Body, &r)
+ if err != nil {
+ return "", err
+ }
+
+ return gophercloud.ExtractNextURL(r.Links)
+}
+
+// ExtractDBs will convert a generic pagination struct into a more
+// relevant slice of DB structs.
+func ExtractDBs(page pagination.Page) ([]db.Database, error) {
+ casted := page.(AccessPage).Body
+
+ var response struct {
+ DBs []db.Database `mapstructure:"databases"`
+ }
+
+ err := mapstructure.Decode(casted, &response)
+ return response.DBs, err
+}
+
+// UserPage represents a single page of a paginated user collection.
+type UserPage struct {
+ pagination.LinkedPageBase
+}
+
+// IsEmpty checks to see whether the collection is empty.
+func (page UserPage) IsEmpty() (bool, error) {
+ users, err := ExtractUsers(page)
+ if err != nil {
+ return true, err
+ }
+ return len(users) == 0, nil
+}
+
+// NextPageURL will retrieve the next page URL.
+func (page UserPage) NextPageURL() (string, error) {
+ type resp struct {
+ Links []gophercloud.Link `mapstructure:"users_links"`
+ }
+
+ var r resp
+ err := mapstructure.Decode(page.Body, &r)
+ if err != nil {
+ return "", err
+ }
+
+ return gophercloud.ExtractNextURL(r.Links)
+}
+
+// ExtractUsers will convert a generic pagination struct into a more
+// relevant slice of User structs.
+func ExtractUsers(page pagination.Page) ([]User, error) {
+ casted := page.(UserPage).Body
+
+ var response struct {
+ Users []User `mapstructure:"users"`
+ }
+
+ err := mapstructure.Decode(casted, &response)
+
+ return response.Users, err
+}
+
+// GrantAccessResult represents the result of granting access to a user.
+type GrantAccessResult struct {
+ gophercloud.ErrResult
+}
+
+// RevokeAccessResult represents the result of revoking access to a user.
+type RevokeAccessResult struct {
+ gophercloud.ErrResult
+}
diff --git a/rackspace/db/v1/users/urls.go b/rackspace/db/v1/users/urls.go
new file mode 100644
index 0000000..bac8788
--- /dev/null
+++ b/rackspace/db/v1/users/urls.go
@@ -0,0 +1,19 @@
+package users
+
+import "github.com/rackspace/gophercloud"
+
+func baseURL(c *gophercloud.ServiceClient, instanceID string) string {
+ return c.ServiceURL("instances", instanceID, "users")
+}
+
+func userURL(c *gophercloud.ServiceClient, instanceID, userName string) string {
+ return c.ServiceURL("instances", instanceID, "users", userName)
+}
+
+func dbsURL(c *gophercloud.ServiceClient, instanceID, userName string) string {
+ return c.ServiceURL("instances", instanceID, "users", userName, "databases")
+}
+
+func dbURL(c *gophercloud.ServiceClient, instanceID, userName, dbName string) string {
+ return c.ServiceURL("instances", instanceID, "users", userName, "databases", dbName)
+}
diff --git a/testhelper/fixture/helper.go b/testhelper/fixture/helper.go
new file mode 100644
index 0000000..d54355d
--- /dev/null
+++ b/testhelper/fixture/helper.go
@@ -0,0 +1,31 @@
+package fixture
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+
+ th "github.com/rackspace/gophercloud/testhelper"
+ "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func SetupHandler(t *testing.T, url, method, requestBody, responseBody string, status int) {
+ th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, method)
+ th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+
+ if requestBody != "" {
+ th.TestJSONRequest(t, r, requestBody)
+ }
+
+ if responseBody != "" {
+ w.Header().Add("Content-Type", "application/json")
+ }
+
+ w.WriteHeader(status)
+
+ if responseBody != "" {
+ fmt.Fprintf(w, responseBody)
+ }
+ })
+}