Add support for OpenStack DBs
diff --git a/openstack/db/v1/databases/doc.go b/openstack/db/v1/databases/doc.go
new file mode 100644
index 0000000..18cbec7
--- /dev/null
+++ b/openstack/db/v1/databases/doc.go
@@ -0,0 +1 @@
+package databases
diff --git a/openstack/db/v1/databases/fixtures.go b/openstack/db/v1/databases/fixtures.go
new file mode 100644
index 0000000..d03466f
--- /dev/null
+++ b/openstack/db/v1/databases/fixtures.go
@@ -0,0 +1,76 @@
+package databases
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	th "github.com/rackspace/gophercloud/testhelper"
+	fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func HandleCreateDBSuccessfully(t *testing.T, instanceID string) {
+	th.Mux.HandleFunc("/instances/"+instanceID+"/databases", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "POST")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+		th.TestJSONRequest(t, r, `
+{
+  "databases": [
+    {
+      "character_set": "utf8",
+      "collate": "utf8_general_ci",
+      "name": "testingdb"
+    },
+    {
+      "name": "sampledb"
+    }
+  ]
+}
+`)
+
+		w.Header().Set("Content-Type", "application/json")
+		w.WriteHeader(http.StatusAccepted)
+	})
+}
+
+func HandleListDBsSuccessfully(t *testing.T, instanceID string) {
+	th.Mux.HandleFunc("/instances/"+instanceID+"/databases", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+		w.Header().Set("Content-Type", "application/json")
+		w.WriteHeader(http.StatusOK)
+
+		fmt.Fprintf(w, `
+{
+	"databases": [
+		{
+			"name": "anotherexampledb"
+		},
+		{
+			"name": "exampledb"
+		},
+		{
+			"name": "nextround"
+		},
+		{
+			"name": "sampledb"
+		},
+		{
+			"name": "testingdb"
+		}
+	]
+}
+`)
+	})
+}
+
+func HandleDeleteDBSuccessfully(t *testing.T, instanceID, dbName string) {
+	th.Mux.HandleFunc("/instances/"+instanceID+"/databases/"+dbName, func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "DELETE")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+		w.Header().Set("Content-Type", "application/json")
+		w.WriteHeader(http.StatusAccepted)
+	})
+}
diff --git a/openstack/db/v1/databases/requests.go b/openstack/db/v1/databases/requests.go
new file mode 100644
index 0000000..300e177
--- /dev/null
+++ b/openstack/db/v1/databases/requests.go
@@ -0,0 +1,105 @@
+package databases
+
+import (
+	"fmt"
+
+	"github.com/racker/perigee"
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+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 {
+	// Specifies the name of the database. Optional.
+	Name string
+
+	// Set of symbols and encodings. Optional; the default character set is utf8.
+	CharSet string
+
+	// Set of rules for comparing characters in a character set. Optional; the
+	// default value for collate is utf8_general_ci.
+	Collate string
+}
+
+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
+}
+
+type BatchCreateOpts []CreateOpts
+
+func (opts BatchCreateOpts) ToDBCreateMap() (map[string]interface{}, error) {
+	var dbs []map[string]string
+	for _, db := range opts {
+		dbMap, err := db.ToMap()
+		if err != nil {
+			return nil, err
+		}
+		dbs = append(dbs, dbMap)
+	}
+	return map[string]interface{}{"databases": dbs}, nil
+}
+
+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
+	}
+
+	resp, err := perigee.Request("POST", baseURL(client, instanceID), perigee.Options{
+		MoreHeaders: client.AuthenticatedHeaders(),
+		ReqBody:     &reqBody,
+		Results:     &res.Body,
+		OkCodes:     []int{202},
+	})
+
+	res.Header = resp.HttpResponse.Header
+	res.Err = err
+
+	return res
+}
+
+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)
+}
+
+func Delete(client *gophercloud.ServiceClient, instanceID, dbName string) DeleteResult {
+	var res DeleteResult
+
+	resp, err := perigee.Request("DELETE", dbURL(client, instanceID, dbName), perigee.Options{
+		MoreHeaders: client.AuthenticatedHeaders(),
+		Results:     &res.Body,
+		OkCodes:     []int{202},
+	})
+
+	res.Header = resp.HttpResponse.Header
+	res.Err = err
+
+	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..6f5edb6
--- /dev/null
+++ b/openstack/db/v1/databases/requests_test.go
@@ -0,0 +1,71 @@
+package databases
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud/pagination"
+	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()
+
+	HandleCreateDBSuccessfully(t, instanceID)
+
+	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()
+
+	HandleListDBsSuccessfully(t, instanceID)
+
+	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()
+
+	HandleDeleteDBSuccessfully(t, instanceID, "{dbName}")
+
+	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..85e3363
--- /dev/null
+++ b/openstack/db/v1/databases/results.go
@@ -0,0 +1,69 @@
+package databases
+
+import (
+	"github.com/mitchellh/mapstructure"
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+type Database struct {
+	// Specifies the name of the MySQL DB.
+	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
+}
+
+type CreateResult struct {
+	gophercloud.Result
+}
+
+// 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
+}
+
+type DeleteResult struct {
+	gophercloud.ErrResult
+}
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)
+}