openstack object storage v02.0
diff --git a/acceptance/openstack/compute_test.go b/acceptance/openstack/compute_test.go
index 4c487c7..8d5fe28 100644
--- a/acceptance/openstack/compute_test.go
+++ b/acceptance/openstack/compute_test.go
@@ -11,8 +11,10 @@
 	"testing"
 )
 
+var service = "compute"
+
 func TestListServers(t *testing.T) {
-	ts, err := setupForList()
+	ts, err := setupForList(service)
 	if err != nil {
 		t.Error(err)
 		return
@@ -52,7 +54,7 @@
 }
 
 func TestListImages(t *testing.T) {
-	ts, err := setupForList()
+	ts, err := setupForList(service)
 	if err != nil {
 		t.Error(err)
 		return
@@ -92,7 +94,7 @@
 }
 
 func TestListFlavors(t *testing.T) {
-	ts, err := setupForList()
+	ts, err := setupForList(service)
 	if err != nil {
 		t.Error(err)
 		return
diff --git a/acceptance/openstack/storage_test.go b/acceptance/openstack/storage_test.go
new file mode 100644
index 0000000..faabf6a
--- /dev/null
+++ b/acceptance/openstack/storage_test.go
@@ -0,0 +1,352 @@
+// +build acceptance
+
+package openstack
+
+import (
+	"bytes"
+	"github.com/rackspace/gophercloud/openstack/storage"
+	"github.com/rackspace/gophercloud/openstack/storage/accounts"
+	"github.com/rackspace/gophercloud/openstack/storage/containers"
+	"github.com/rackspace/gophercloud/openstack/storage/objects"
+	"os"
+	"strings"
+	"testing"
+)
+
+var objectStorage = "object-store"
+var metadata = map[string]string{"gopher": "cloud"}
+var numContainers = 2
+var numObjects = 2
+
+func TestAccount(t *testing.T) {
+	ts, err := setupForList(objectStorage)
+	if err != nil {
+		t.Error(err)
+		return
+	}
+
+	region := os.Getenv("OS_REGION_NAME")
+	for _, ep := range ts.eps {
+		if (region != "") && (region != ep.Region) {
+			continue
+		}
+
+		client := storage.NewClient(ep.PublicURL, ts.a, ts.o)
+
+		err := accounts.Update(client, accounts.UpdateOpts{
+			Metadata: metadata,
+		})
+		if err != nil {
+			t.Error(err)
+			return
+		}
+		defer func() {
+			tempMap := make(map[string]string)
+			for k := range metadata {
+				tempMap[k] = ""
+			}
+			err = accounts.Update(client, accounts.UpdateOpts{
+				Metadata: tempMap,
+			})
+			if err != nil {
+				t.Error(err)
+				return
+			}
+		}()
+
+		gr, err := accounts.Get(client, accounts.GetOpts{})
+		if err != nil {
+			t.Error(err)
+			return
+		}
+		am := accounts.GetMetadata(gr)
+		for k := range metadata {
+			if am[k] != metadata[strings.Title(k)] {
+				t.Errorf("Expected custom metadata with key: %s", k)
+				return
+			}
+		}
+	}
+}
+
+func TestContainers(t *testing.T) {
+	ts, err := setupForList(objectStorage)
+	if err != nil {
+		t.Error(err)
+		return
+	}
+
+	region := os.Getenv("OS_REGION_NAME")
+	for _, ep := range ts.eps {
+		if (region != "") && (region != ep.Region) {
+			continue
+		}
+
+		client := storage.NewClient(ep.PublicURL, ts.a, ts.o)
+
+		cNames := make([]string, numContainers)
+		for i := 0; i < numContainers; i++ {
+			cNames[i] = randomString("test-container-", 8)
+		}
+
+		for i := 0; i < len(cNames); i++ {
+			_, err := containers.Create(client, containers.CreateOpts{
+				Name: cNames[i],
+			})
+			if err != nil {
+				t.Error(err)
+				return
+			}
+		}
+		defer func() {
+			for i := 0; i < len(cNames); i++ {
+				err = containers.Delete(client, containers.DeleteOpts{
+					Name: cNames[i],
+				})
+				if err != nil {
+					t.Error(err)
+					return
+				}
+			}
+		}()
+
+		lr, err := containers.List(client, containers.ListOpts{
+			Full: false,
+		})
+		if err != nil {
+			t.Error(err)
+			return
+		}
+		cns, err := containers.GetNames(lr)
+		if err != nil {
+			t.Error(err)
+			return
+		}
+		if len(cns) != len(cNames) {
+			t.Errorf("Expected %d names and got %d", len(cNames), len(cns))
+			return
+		}
+
+		lr, err = containers.List(client, containers.ListOpts{
+			Full: true,
+		})
+		if err != nil {
+			t.Error(err)
+			return
+		}
+		cis, err := containers.GetInfo(lr)
+		if err != nil {
+			t.Error(err)
+			return
+		}
+		if len(cis) != len(cNames) {
+			t.Errorf("Expected %d containers and got %d", len(cNames), len(cis))
+			return
+		}
+
+		err = containers.Update(client, containers.UpdateOpts{
+			Name:     cNames[0],
+			Metadata: metadata,
+		})
+		if err != nil {
+			t.Error(err)
+			return
+		}
+		defer func() {
+			tempMap := make(map[string]string)
+			for k := range metadata {
+				tempMap[k] = ""
+			}
+			err = containers.Update(client, containers.UpdateOpts{
+				Name:     cNames[0],
+				Metadata: tempMap,
+			})
+			if err != nil {
+				t.Error(err)
+				return
+			}
+		}()
+
+		gr, err := containers.Get(client, containers.GetOpts{})
+		if err != nil {
+			t.Error(err)
+			return
+		}
+		cm := containers.GetMetadata(gr)
+		for k := range metadata {
+			if cm[k] != metadata[strings.Title(k)] {
+				t.Errorf("Expected custom metadata with key: %s", k)
+				return
+			}
+		}
+	}
+}
+
+func TestObjects(t *testing.T) {
+	ts, err := setupForList(objectStorage)
+	if err != nil {
+		t.Error(err)
+		return
+	}
+
+	region := os.Getenv("OS_REGION_NAME")
+
+	for _, ep := range ts.eps {
+		if (region != "") && (region != ep.Region) {
+			continue
+		}
+
+		client := storage.NewClient(ep.PublicURL, ts.a, ts.o)
+
+		oNames := make([]string, numObjects)
+		for i := 0; i < len(oNames); i++ {
+			oNames[i] = randomString("test-object-", 8)
+		}
+
+		cName := randomString("test-container-", 8)
+		_, err := containers.Create(client, containers.CreateOpts{
+			Name: cName,
+		})
+		if err != nil {
+			t.Error(err)
+			return
+		}
+		defer func() {
+			err = containers.Delete(client, containers.DeleteOpts{
+				Name: cName,
+			})
+			if err != nil {
+				t.Error(err)
+				return
+			}
+		}()
+
+		oContents := make([]*bytes.Buffer, numObjects)
+		for i := 0; i < numObjects; i++ {
+			oContents[i] = bytes.NewBuffer([]byte(randomString("", 10)))
+			err = objects.Create(client, objects.CreateOpts{
+				Container: cName,
+				Name:      oNames[i],
+				Content:   oContents[i],
+			})
+			if err != nil {
+				t.Error(err)
+				return
+			}
+		}
+		defer func() {
+			for i := 0; i < numObjects; i++ {
+				err = objects.Delete(client, objects.DeleteOpts{
+					Container: cName,
+					Name:      oNames[i],
+				})
+			}
+		}()
+
+		lr, err := objects.List(client, objects.ListOpts{
+			Full:      false,
+			Container: cName,
+		})
+		if err != nil {
+			t.Error(err)
+			return
+		}
+		ons := objects.GetNames(lr)
+		if len(ons) != len(oNames) {
+			t.Errorf("Expected %d names and got %d", len(oNames), len(ons))
+			return
+		}
+
+		lr, err = objects.List(client, objects.ListOpts{
+			Full:      true,
+			Container: cName,
+		})
+		if err != nil {
+			t.Error(err)
+			return
+		}
+		ois, err := objects.GetInfo(lr)
+		if err != nil {
+			t.Error(err)
+			return
+		}
+		if len(ois) != len(oNames) {
+			t.Errorf("Expected %d containers and got %d", len(oNames), len(ois))
+			return
+		}
+
+		err = objects.Copy(client, objects.CopyOpts{
+			Container:    cName,
+			Name:         oNames[0],
+			NewContainer: cName,
+			NewName:      oNames[1],
+		})
+		if err != nil {
+			t.Error(err)
+			return
+		}
+
+		dr, err := objects.Download(client, objects.DownloadOpts{
+			Container: cName,
+			Name:      oNames[1],
+		})
+		if err != nil {
+			t.Error(err)
+			return
+		}
+		o2Content := objects.GetContent(dr)
+
+		dr, err = objects.Download(client, objects.DownloadOpts{
+			Container: cName,
+			Name:      oNames[0],
+		})
+		if err != nil {
+			t.Error(err)
+			return
+		}
+		o1Content := objects.GetContent(dr)
+
+		if string(o2Content) != string(o1Content) {
+			t.Errorf("Copy failed. Expected\n%s\nand got\n%s", string(o1Content), string(o2Content))
+			return
+		}
+
+		err = objects.Update(client, objects.UpdateOpts{
+			Container: cName,
+			Name:      oNames[0],
+			Metadata:  metadata,
+		})
+		if err != nil {
+			t.Error(err)
+			return
+		}
+		defer func() {
+			tempMap := make(map[string]string)
+			for k := range metadata {
+				tempMap[k] = ""
+			}
+			err = objects.Update(client, objects.UpdateOpts{
+				Container: cName,
+				Name:      oNames[0],
+				Metadata:  tempMap,
+			})
+			if err != nil {
+				t.Error(err)
+				return
+			}
+		}()
+
+		gr, err := objects.Get(client, objects.GetOpts{})
+		if err != nil {
+			t.Error(err)
+			return
+		}
+		om := objects.GetMetadata(gr)
+		for k := range metadata {
+			if om[k] != metadata[strings.Title(k)] {
+				t.Errorf("Expected custom metadata with key: %s", k)
+				return
+			}
+		}
+	}
+}
diff --git a/acceptance/openstack/tools_test.go b/acceptance/openstack/tools_test.go
index c9a004f..d27ab0a 100644
--- a/acceptance/openstack/tools_test.go
+++ b/acceptance/openstack/tools_test.go
@@ -33,7 +33,7 @@
 	alternateName string
 }
 
-func setupForList() (*testState, error) {
+func setupForList(service string) (*testState, error) {
 	var err error
 
 	ts := new(testState)
@@ -53,7 +53,7 @@
 		return ts, err
 	}
 
-	ts.eps, err = findAllComputeEndpoints(ts.sc)
+	ts.eps, err = findAllEndpoints(ts.sc, service)
 	if err != nil {
 		return ts, err
 	}
@@ -65,7 +65,7 @@
 }
 
 func setupForCRUD() (*testState, error) {
-	ts, err := setupForList()
+	ts, err := setupForList("compute")
 	if err != nil {
 		return ts, err
 	}
@@ -93,19 +93,19 @@
 	return ts, err
 }
 
-func findAllComputeEndpoints(sc *identity.ServiceCatalog) ([]identity.Endpoint, error) {
+func findAllEndpoints(sc *identity.ServiceCatalog, service string) ([]identity.Endpoint, error) {
 	ces, err := sc.CatalogEntries()
 	if err != nil {
 		return nil, err
 	}
 
 	for _, ce := range ces {
-		if ce.Type == "compute" {
+		if ce.Type == service {
 			return ce.Endpoints, nil
 		}
 	}
 
-	return nil, fmt.Errorf("Compute endpoint not found.")
+	return nil, fmt.Errorf(service + " endpoint not found.")
 }
 
 func findEndpointForRegion(eps []identity.Endpoint, r string) (string, error) {
diff --git a/api_fetch.go b/api_fetch.go
new file mode 100644
index 0000000..493f61e
--- /dev/null
+++ b/api_fetch.go
@@ -0,0 +1,41 @@
+package gophercloud
+
+import (
+	"github.com/mitchellh/mapstructure"
+)
+
+//The default generic openstack api
+var OpenstackApi = map[string]interface{}{
+	"UrlChoice": PublicURL,
+}
+
+// Api for use with rackspace
+var RackspaceApi = map[string]interface{}{
+	"Name":      "cloudServersOpenStack",
+	"VersionId": "2",
+	"UrlChoice": PublicURL,
+}
+
+//Populates an ApiCriteria struct with the api values
+//from one of the api maps
+func PopulateApi(variant string) (ApiCriteria, error) {
+	var Api ApiCriteria
+	var variantMap map[string]interface{}
+
+	switch variant {
+	case "":
+		variantMap = OpenstackApi
+
+	case "openstack":
+		variantMap = OpenstackApi
+
+	case "rackspace":
+		variantMap = RackspaceApi
+	}
+
+	err := mapstructure.Decode(variantMap, &Api)
+	if err != nil {
+		return Api, err
+	}
+	return Api, err
+}
diff --git a/floating_ips.go b/floating_ips.go
index 4c27347..1163667 100644
--- a/floating_ips.go
+++ b/floating_ips.go
@@ -1,6 +1,7 @@
 package gophercloud
 
 import (
+	"errors"
 	"fmt"
 	"github.com/racker/perigee"
 )
@@ -24,7 +25,7 @@
 }
 
 func (gsp *genericServersProvider) CreateFloatingIp(pool string) (FloatingIp, error) {
-	var fip *FloatingIp
+	fip := new(FloatingIp)
 
 	err := gsp.context.WithReauth(gsp.access, func() error {
 		url := gsp.endpoint + "/os-floating-ips"
@@ -42,6 +43,10 @@
 		})
 	})
 
+	if fip.Ip == "" {
+		return *fip, errors.New("Error creating floating IP")
+	}
+
 	return *fip, err
 }
 
@@ -63,7 +68,7 @@
 
 func (gsp *genericServersProvider) DeleteFloatingIp(ip FloatingIp) error {
 	return gsp.context.WithReauth(gsp.access, func() error {
-		ep := fmt.Sprintf("%s/os-floating-ips/%s", gsp.endpoint, ip.Id)
+		ep := fmt.Sprintf("%s/os-floating-ips/%d", gsp.endpoint, ip.Id)
 		return perigee.Delete(ep, perigee.Options{
 			CustomClient: gsp.context.httpClient,
 			MoreHeaders: map[string]string{
@@ -75,7 +80,7 @@
 }
 
 type FloatingIp struct {
-	Id         string `json:"id"`
+	Id         int    `json:"id"`
 	Pool       string `json:"pool"`
 	Ip         string `json:"ip"`
 	FixedIp    string `json:"fixed_ip"`
diff --git a/interfaces.go b/interfaces.go
index 6340c1c..b038aa7 100644
--- a/interfaces.go
+++ b/interfaces.go
@@ -193,4 +193,25 @@
 
 	// ShowKeyPair will yield the named keypair.
 	ShowKeyPair(name string) (KeyPair, error)
+
+	// ListSecurityGroups provides a listing of security groups for the tenant.
+	// This method works only if the provider supports the os-security-groups extension.
+	ListSecurityGroups() ([]SecurityGroup, error)
+
+	// CreateSecurityGroup lets a tenant create a new security group.
+	// Only the SecurityGroup fields which are specified will be marshalled to the API.
+	// This method works only if the provider supports the os-security-groups extension.
+	CreateSecurityGroup(desired SecurityGroup) (*SecurityGroup, error)
+
+	// ListSecurityGroupsByServerId provides a list of security groups which apply to the indicated server.
+	// This method works only if the provider supports the os-security-groups extension.
+	ListSecurityGroupsByServerId(id string) ([]SecurityGroup, error)
+
+	// SecurityGroupById returns a security group corresponding to the provided ID number.
+	// This method works only if the provider supports the os-security-groups extension.
+	SecurityGroupById(id int) (*SecurityGroup, error)
+
+	// DeleteSecurityGroupById disposes of a security group corresponding to the provided ID number.
+	// This method works only if the provider supports the os-security-groups extension.
+	DeleteSecurityGroupById(id int) error
 }
diff --git a/openstack/storage/accounts/accounts.go b/openstack/storage/accounts/accounts.go
new file mode 100644
index 0000000..cb72072
--- /dev/null
+++ b/openstack/storage/accounts/accounts.go
@@ -0,0 +1,27 @@
+package accounts
+
+import (
+	"strings"
+)
+
+type UpdateOpts struct {
+	Metadata map[string]string
+	Headers  map[string]string
+}
+
+type GetOpts struct {
+	Headers map[string]string
+}
+
+// GetMetadata is a function that takes a GetResult (of type *perigee.Response)
+// and returns the custom metatdata associated with the account.
+func GetMetadata(gr GetResult) map[string]string {
+	metadata := make(map[string]string)
+	for k, v := range gr.HttpResponse.Header {
+		if strings.HasPrefix(k, "X-Account-Meta-") {
+			key := strings.TrimPrefix(k, "X-Account-Meta-")
+			metadata[key] = v[0]
+		}
+	}
+	return metadata
+}
diff --git a/openstack/storage/accounts/requests.go b/openstack/storage/accounts/requests.go
new file mode 100644
index 0000000..5c658d7
--- /dev/null
+++ b/openstack/storage/accounts/requests.go
@@ -0,0 +1,50 @@
+package accounts
+
+import (
+	"github.com/racker/perigee"
+	"github.com/rackspace/gophercloud/openstack/storage"
+)
+
+type GetResult *perigee.Response
+
+// Update is a function that creates, updates, or deletes an account's metadata.
+func Update(c *storage.Client, opts UpdateOpts) error {
+	h, err := c.GetHeaders()
+	if err != nil {
+		return err
+	}
+	for k, v := range opts.Headers {
+		h[k] = v
+	}
+
+	for k, v := range opts.Metadata {
+		h["X-Account-Meta-"+k] = v
+	}
+
+	url := c.GetAccountURL()
+	_, err = perigee.Request("POST", url, perigee.Options{
+		MoreHeaders: h,
+		OkCodes:     []int{204},
+	})
+	return err
+}
+
+// Get is a function that retrieves an account's metadata. To extract just the custom
+// metadata, pass the GetResult response to the GetMetadata function.
+func Get(c *storage.Client, opts GetOpts) (GetResult, error) {
+	h, err := c.GetHeaders()
+	if err != nil {
+		return nil, err
+	}
+
+	for k, v := range opts.Headers {
+		h[k] = v
+	}
+
+	url := c.GetAccountURL()
+	resp, err := perigee.Request("HEAD", url, perigee.Options{
+		MoreHeaders: h,
+		OkCodes:     []int{204},
+	})
+	return resp, err
+}
diff --git a/openstack/storage/client.go b/openstack/storage/client.go
new file mode 100644
index 0000000..94307b0
--- /dev/null
+++ b/openstack/storage/client.go
@@ -0,0 +1,60 @@
+package storage
+
+import (
+	"fmt"
+	"github.com/rackspace/gophercloud/openstack/identity"
+)
+
+// Client is a structure that contains information for communicating with a provider.
+type Client struct {
+	endpoint  string
+	authority identity.AuthResults
+	options   identity.AuthOptions
+	token     *identity.Token
+}
+
+func NewClient(e string, a identity.AuthResults, o identity.AuthOptions) *Client {
+	return &Client{
+		endpoint:  e,
+		authority: a,
+		options:   o,
+	}
+}
+
+func (c *Client) GetAccountURL() string {
+	return fmt.Sprintf("%s", c.endpoint)
+}
+
+func (c *Client) GetContainerURL(container string) string {
+	return fmt.Sprintf("%s/%s", c.endpoint, container)
+}
+
+func (c *Client) GetObjectURL(container, object string) string {
+	return fmt.Sprintf("%s/%s/%s", c.endpoint, container, object)
+}
+
+// GetHeaders is a function that sets the header for token authentication against a client's endpoint.
+func (c *Client) GetHeaders() (map[string]string, error) {
+	t, err := c.getAuthToken()
+	if err != nil {
+		return map[string]string{}, err
+	}
+
+	return map[string]string{
+		"X-Auth-Token": t,
+	}, nil
+}
+
+// getAuthToken is a function that tries to retrieve an authentication token from a client's endpoint.
+func (c *Client) getAuthToken() (string, error) {
+	var err error
+
+	if c.token == nil {
+		c.token, err = identity.GetToken(c.authority)
+		if err != nil {
+			return "", err
+		}
+	}
+
+	return c.token.Id, err
+}
diff --git a/openstack/storage/containers/containers.go b/openstack/storage/containers/containers.go
new file mode 100644
index 0000000..b3fb921
--- /dev/null
+++ b/openstack/storage/containers/containers.go
@@ -0,0 +1,69 @@
+package containers
+
+import (
+	"encoding/json"
+	"strings"
+)
+
+type Container struct {
+	Bytes int
+	Count int
+	Name  string
+}
+
+type ListOpts struct {
+	Full   bool
+	Params map[string]string
+}
+
+type CreateOpts struct {
+	Name     string
+	Metadata map[string]string
+	Headers  map[string]string
+}
+
+type DeleteOpts struct {
+	Name   string
+	Params map[string]string
+}
+
+type UpdateOpts struct {
+	Name     string
+	Metadata map[string]string
+	Headers  map[string]string
+}
+
+type GetOpts struct {
+	Name     string
+	Metadata map[string]string
+}
+
+// GetInfo is a function that takes a ListResult (of type *perigee.Response)
+// and returns the containers' information.
+func GetInfo(lr ListResult) ([]Container, error) {
+	var ci []Container
+	err := json.Unmarshal(lr.JsonResult, &ci)
+	return ci, err
+}
+
+// GetNames is a function that takes a ListResult (of type *perigee.Response)
+// and returns the containers' names.
+func GetNames(lr ListResult) ([]string, error) {
+	jr := string(lr.JsonResult)
+	cns := strings.Split(jr, "\n")
+	cns = cns[:len(cns)-1]
+	return cns, nil
+}
+
+// GetMetadata is a function that takes a GetResult (of type *perigee.Response)
+// and returns the custom metadata associated with the container.
+func GetMetadata(gr GetResult) map[string]string {
+	metadata := make(map[string]string)
+	for k, v := range gr.HttpResponse.Header {
+		if strings.HasPrefix(k, "X-Container-Meta-") {
+			key := strings.TrimPrefix(k, "X-Container-Meta-")
+			metadata[key] = v[0]
+		}
+	}
+	return metadata
+}
diff --git a/openstack/storage/containers/requests.go b/openstack/storage/containers/requests.go
new file mode 100644
index 0000000..6cdd7c7
--- /dev/null
+++ b/openstack/storage/containers/requests.go
@@ -0,0 +1,127 @@
+package containers
+
+import (
+	"github.com/racker/perigee"
+	"github.com/rackspace/gophercloud/openstack/storage"
+	"github.com/rackspace/gophercloud/openstack/utils"
+)
+
+type ListResult *perigee.Response
+type GetResult *perigee.Response
+
+// List is a function that retrieves all objects in a container. It also returns the details
+// for the account. To extract just the container information or names, pass the ListResult
+// response to the GetInfo or GetNames function, respectively.
+func List(c *storage.Client, opts ListOpts) (ListResult, error) {
+	contentType := ""
+
+	h, err := c.GetHeaders()
+	if err != nil {
+		return nil, err
+	}
+
+	query := utils.BuildQuery(opts.Params)
+
+	if !opts.Full {
+		contentType = "text/plain"
+	}
+
+	url := c.GetAccountURL() + query
+	resp, err := perigee.Request("GET", url, perigee.Options{
+		Results:     true,
+		MoreHeaders: h,
+		OkCodes:     []int{200, 204},
+		Accept:      contentType,
+	})
+	return resp, err
+}
+
+// Create is a function that creates a new container.
+func Create(c *storage.Client, opts CreateOpts) (*Container, error) {
+	var ci *Container
+
+	h, err := c.GetHeaders()
+	if err != nil {
+		return new(Container), err
+	}
+
+	for k, v := range opts.Headers {
+		h[k] = v
+	}
+
+	for k, v := range opts.Metadata {
+		h["X-Container-Meta-"+k] = v
+	}
+
+	url := c.GetContainerURL(opts.Name)
+	_, err = perigee.Request("PUT", url, perigee.Options{
+		MoreHeaders: h,
+		OkCodes:     []int{201, 204},
+	})
+	if err == nil {
+		ci = &Container{
+			Name: opts.Name,
+		}
+	}
+	return ci, err
+}
+
+// Delete is a function that deletes a container.
+func Delete(c *storage.Client, opts DeleteOpts) error {
+	h, err := c.GetHeaders()
+	if err != nil {
+		return err
+	}
+
+	query := utils.BuildQuery(opts.Params)
+
+	url := c.GetContainerURL(opts.Name) + query
+	_, err = perigee.Request("DELETE", url, perigee.Options{
+		MoreHeaders: h,
+		OkCodes:     []int{204},
+	})
+	return err
+}
+
+// Update is a function that creates, updates, or deletes a container's metadata.
+func Update(c *storage.Client, opts UpdateOpts) error {
+	h, err := c.GetHeaders()
+	if err != nil {
+		return err
+	}
+
+	for k, v := range opts.Headers {
+		h[k] = v
+	}
+
+	for k, v := range opts.Metadata {
+		h["X-Container-Meta-"+k] = v
+	}
+
+	url := c.GetContainerURL(opts.Name)
+	_, err = perigee.Request("POST", url, perigee.Options{
+		MoreHeaders: h,
+		OkCodes:     []int{204},
+	})
+	return err
+}
+
+// Get is a function that retrieves the metadata of a container. To extract just the custom
+// metadata, pass the GetResult response to the GetMetadata function.
+func Get(c *storage.Client, opts GetOpts) (GetResult, error) {
+	h, err := c.GetHeaders()
+	if err != nil {
+		return nil, err
+	}
+
+	for k, v := range opts.Metadata {
+		h["X-Container-Meta-"+k] = v
+	}
+
+	url := c.GetContainerURL(opts.Name)
+	resp, err := perigee.Request("HEAD", url, perigee.Options{
+		MoreHeaders: h,
+		OkCodes:     []int{204},
+	})
+	return resp, err
+}
diff --git a/openstack/storage/objects/objects.go b/openstack/storage/objects/objects.go
new file mode 100644
index 0000000..20228e5
--- /dev/null
+++ b/openstack/storage/objects/objects.go
@@ -0,0 +1,102 @@
+package objects
+
+import (
+	"bytes"
+	"encoding/json"
+	"strings"
+)
+
+type Object struct {
+	Name          string
+	Hash          string
+	Bytes         int
+	Content_type  string
+	Last_modified string
+}
+
+type ListOpts struct {
+	Container string
+	Full      bool
+	Params    map[string]string
+}
+
+type DownloadOpts struct {
+	Container string
+	Name      string
+	Headers   map[string]string
+	Params    map[string]string
+}
+
+type CreateOpts struct {
+	Container string
+	Name      string
+	Content   *bytes.Buffer
+	Metadata  map[string]string
+	Headers   map[string]string
+	Params    map[string]string
+}
+
+type CopyOpts struct {
+	Container    string
+	Name         string
+	NewContainer string
+	NewName      string
+	Metadata     map[string]string
+	Headers      map[string]string
+}
+
+type DeleteOpts struct {
+	Container string
+	Name      string
+	Params    map[string]string
+}
+
+type GetOpts struct {
+	Container string
+	Name      string
+	Headers   map[string]string
+	Params    map[string]string
+}
+
+type UpdateOpts struct {
+	Container string
+	Name      string
+	Metadata  map[string]string
+	Headers   map[string]string
+}
+
+// GetInfo is a function that takes a ListResult (of type *perigee.Response)
+// and returns the objects' information.
+func GetInfo(lr ListResult) ([]Object, error) {
+	var oi []Object
+	err := json.Unmarshal(lr.JsonResult, &oi)
+	return oi, err
+}
+
+// GetNames is a function that takes a ListResult (of type *perigee.Response)
+// and returns the objects' names.
+func GetNames(lr ListResult) []string {
+	jr := string(lr.JsonResult)
+	ons := strings.Split(jr, "\n")
+	ons = ons[:len(ons)-1]
+	return ons
+}
+
+// GetContent is a function that takes a DownloadResult (of type *perigee.Response)
+// and returns the object's content.
+func GetContent(dr DownloadResult) []byte {
+	return dr.JsonResult
+}
+
+// GetMetadata is a function that takes a GetResult (of type *perifee.Response)
+// and returns the custom metadata associated with the object.
+func GetMetadata(gr GetResult) map[string]string {
+	metadata := make(map[string]string)
+	for k, v := range gr.HttpResponse.Header {
+		if strings.HasPrefix(k, "X-Object-Meta-") {
+			key := strings.TrimPrefix(k, "X-Object-Meta-")
+			metadata[key] = v[0]
+		}
+	}
+	return metadata
+}
diff --git a/openstack/storage/objects/requests.go b/openstack/storage/objects/requests.go
new file mode 100644
index 0000000..8f645fc
--- /dev/null
+++ b/openstack/storage/objects/requests.go
@@ -0,0 +1,182 @@
+package objects
+
+import (
+	"fmt"
+
+	"github.com/racker/perigee"
+	"github.com/rackspace/gophercloud/openstack/storage"
+	"github.com/rackspace/gophercloud/openstack/utils"
+)
+
+type ListResult *perigee.Response
+type DownloadResult *perigee.Response
+type GetResult *perigee.Response
+
+// List is a function that retrieves all objects in a container. It also returns the details
+// for the container. To extract only the object information or names, pass the ListResult
+// response to the GetInfo or GetNames function, respectively.
+func List(c *storage.Client, opts ListOpts) (ListResult, error) {
+	contentType := ""
+
+	h, err := c.GetHeaders()
+	if err != nil {
+		return nil, err
+	}
+
+	query := utils.BuildQuery(opts.Params)
+
+	if !opts.Full {
+		contentType = "text/plain"
+	}
+
+	url := c.GetContainerURL(opts.Container) + query
+	resp, err := perigee.Request("GET", url, perigee.Options{
+		Results:     true,
+		MoreHeaders: h,
+		OkCodes:     []int{200, 204},
+		Accept:      contentType,
+	})
+	return resp, err
+}
+
+// Download is a function that retrieves the content and metadata for an object.
+// To extract just the content, pass the DownloadResult response to the GetContent
+// function.
+func Download(c *storage.Client, opts DownloadOpts) (DownloadResult, error) {
+	h, err := c.GetHeaders()
+	if err != nil {
+		return nil, err
+	}
+
+	for k, v := range opts.Headers {
+		h[k] = v
+	}
+
+	query := utils.BuildQuery(opts.Params)
+
+	url := c.GetObjectURL(opts.Container, opts.Name) + query
+	resp, err := perigee.Request("GET", url, perigee.Options{
+		Results:     true,
+		MoreHeaders: h,
+		OkCodes:     []int{200},
+	})
+	return resp, err
+}
+
+// Create is a function that creates a new object or replaces an existing object.
+func Create(c *storage.Client, opts CreateOpts) error {
+	var reqBody []byte
+
+	h, err := c.GetHeaders()
+	if err != nil {
+		return err
+	}
+
+	for k, v := range opts.Headers {
+		h[k] = v
+	}
+
+	for k, v := range opts.Metadata {
+		h["X-Object-Meta-"+k] = v
+	}
+
+	query := utils.BuildQuery(opts.Params)
+
+	content := opts.Content
+	if content != nil {
+		reqBody = make([]byte, content.Len())
+		_, err = content.Read(reqBody)
+		if err != nil {
+			return err
+		}
+	}
+
+	url := c.GetObjectURL(opts.Container, opts.Name) + query
+	_, err = perigee.Request("PUT", url, perigee.Options{
+		ReqBody:     reqBody,
+		MoreHeaders: h,
+		OkCodes:     []int{201},
+	})
+	return err
+}
+
+// Copy is a function that copies one object to another.
+func Copy(c *storage.Client, opts CopyOpts) error {
+	h, err := c.GetHeaders()
+	if err != nil {
+		return err
+	}
+
+	for k, v := range opts.Metadata {
+		h["X-Object-Meta-"+k] = v
+	}
+
+	h["Destination"] = fmt.Sprintf("/%s/%s", opts.NewContainer, opts.NewName)
+
+	url := c.GetObjectURL(opts.Container, opts.Name)
+	_, err = perigee.Request("COPY", url, perigee.Options{
+		MoreHeaders: h,
+		OkCodes:     []int{201},
+	})
+	return err
+}
+
+// Delete is a function that deletes an object.
+func Delete(c *storage.Client, opts DeleteOpts) error {
+	h, err := c.GetHeaders()
+	if err != nil {
+		return err
+	}
+
+	query := utils.BuildQuery(opts.Params)
+
+	url := c.GetObjectURL(opts.Container, opts.Name) + query
+	_, err = perigee.Request("DELETE", url, perigee.Options{
+		MoreHeaders: h,
+		OkCodes:     []int{204},
+	})
+	return err
+}
+
+// Get is a function that retrieves the metadata of an object. To extract just the custom
+// metadata, pass the GetResult response to the GetMetadata function.
+func Get(c *storage.Client, opts GetOpts) (GetResult, error) {
+	h, err := c.GetHeaders()
+	if err != nil {
+		return nil, err
+	}
+
+	for k, v := range opts.Headers {
+		h[k] = v
+	}
+
+	url := c.GetObjectURL(opts.Container, opts.Name)
+	resp, err := perigee.Request("HEAD", url, perigee.Options{
+		MoreHeaders: h,
+		OkCodes:     []int{204},
+	})
+	return resp, err
+}
+
+// Update is a function that creates, updates, or deletes an object's metadata.
+func Update(c *storage.Client, opts UpdateOpts) error {
+	h, err := c.GetHeaders()
+	if err != nil {
+		return err
+	}
+
+	for k, v := range opts.Headers {
+		h[k] = v
+	}
+
+	for k, v := range opts.Metadata {
+		h["X-Object-Meta-"+k] = v
+	}
+
+	url := c.GetObjectURL(opts.Container, opts.Name)
+	_, err = perigee.Request("POST", url, perigee.Options{
+		MoreHeaders: h,
+		OkCodes:     []int{202, 204},
+	})
+	return err
+}
diff --git a/openstack/utils/utils.go b/openstack/utils/utils.go
index 0d4c05c..a814c7e 100644
--- a/openstack/utils/utils.go
+++ b/openstack/utils/utils.go
@@ -50,3 +50,12 @@
 
 	return ao, nil
 }
+
+func BuildQuery(params map[string]string) string {
+	query := "?"
+	for k, v := range params {
+		query += k + "=" + v + "&"
+	}
+	query = query[:len(query)-1]
+	return query
+}
diff --git a/servers.go b/servers.go
index fc5925d..57f1532 100644
--- a/servers.go
+++ b/servers.go
@@ -369,6 +369,114 @@
 	return locationArr[len(locationArr)-1], err
 }
 
+// See the CloudServersProvider interface for details.
+func (gsp *genericServersProvider) ListSecurityGroups() ([]SecurityGroup, error) {
+	var sgs []SecurityGroup
+
+	err := gsp.context.WithReauth(gsp.access, func() error {
+		ep := fmt.Sprintf("%s/os-security-groups", gsp.endpoint)
+		return perigee.Get(ep, perigee.Options{
+			MoreHeaders: map[string]string{
+				"X-Auth-Token": gsp.access.AuthToken(),
+			},
+			Results: &struct {
+				SecurityGroups *[]SecurityGroup `json:"security_groups"`
+			}{&sgs},
+		})
+	})
+	return sgs, err
+}
+
+// See the CloudServersProvider interface for details.
+func (gsp *genericServersProvider) CreateSecurityGroup(desired SecurityGroup) (*SecurityGroup, error) {
+	var actual *SecurityGroup
+
+	err := gsp.context.WithReauth(gsp.access, func() error {
+		ep := fmt.Sprintf("%s/os-security-groups", gsp.endpoint)
+		return perigee.Post(ep, perigee.Options{
+			ReqBody: struct {
+				AddSecurityGroup SecurityGroup `json:"security_group"`
+			}{desired},
+			MoreHeaders: map[string]string{
+				"X-Auth-Token": gsp.access.AuthToken(),
+			},
+			Results: &struct {
+				SecurityGroup **SecurityGroup `json:"security_group"`
+			}{&actual},
+		})
+	})
+	return actual, err
+}
+
+// See the CloudServersProvider interface for details.
+func (gsp *genericServersProvider) ListSecurityGroupsByServerId(id string) ([]SecurityGroup, error) {
+	var sgs []SecurityGroup
+
+	err := gsp.context.WithReauth(gsp.access, func() error {
+		ep := fmt.Sprintf("%s/servers/%s/os-security-groups", gsp.endpoint, id)
+		return perigee.Get(ep, perigee.Options{
+			MoreHeaders: map[string]string{
+				"X-Auth-Token": gsp.access.AuthToken(),
+			},
+			Results: &struct {
+				SecurityGroups *[]SecurityGroup `json:"security_groups"`
+			}{&sgs},
+		})
+	})
+	return sgs, err
+}
+
+// See the CloudServersProvider interface for details.
+func (gsp *genericServersProvider) SecurityGroupById(id int) (*SecurityGroup, error) {
+	var actual *SecurityGroup
+
+	err := gsp.context.WithReauth(gsp.access, func() error {
+		ep := fmt.Sprintf("%s/os-security-groups/%d", gsp.endpoint, id)
+		return perigee.Get(ep, perigee.Options{
+			MoreHeaders: map[string]string{
+				"X-Auth-Token": gsp.access.AuthToken(),
+			},
+			Results: &struct {
+				SecurityGroup **SecurityGroup `json:"security_group"`
+			}{&actual},
+		})
+	})
+	return actual, err
+}
+
+// See the CloudServersProvider interface for details.
+func (gsp *genericServersProvider) DeleteSecurityGroupById(id int) error {
+	err := gsp.context.WithReauth(gsp.access, func() error {
+		ep := fmt.Sprintf("%s/os-security-groups/%d", gsp.endpoint, id)
+		return perigee.Delete(ep, perigee.Options{
+			MoreHeaders: map[string]string{
+				"X-Auth-Token": gsp.access.AuthToken(),
+			},
+			OkCodes: []int{202},
+		})
+	})
+	return err
+}
+
+// SecurityGroup provides a description of a security group, including all its rules.
+type SecurityGroup struct {
+	Description string   `json:"description,omitempty"`
+	Id          int      `json:"id,omitempty"`
+	Name        string   `json:"name,omitempty"`
+	Rules       []SGRule `json:"rules,omitempty"`
+	TenantId    string   `json:"tenant_id,omitempty"`
+}
+
+// SGRule encapsulates a single rule which applies to a security group.
+// This definition is just a guess, based on the documentation found in another extension here: http://docs.openstack.org/api/openstack-compute/2/content/GET_os-security-group-default-rules-v2_listSecGroupDefaultRules_v2__tenant_id__os-security-group-rules_ext-os-security-group-default-rules.html
+type SGRule struct {
+	FromPort   int                    `json:"from_port,omitempty"`
+	Id         int                    `json:"id,omitempty"`
+	IpProtocol string                 `json:"ip_protocol,omitempty"`
+	IpRange    map[string]interface{} `json:"ip_range,omitempty"`
+	ToPort     int                    `json:"to_port,omitempty"`
+}
+
 // RaxBandwidth provides measurement of server bandwidth consumed over a given audit interval.
 type RaxBandwidth struct {
 	AuditPeriodEnd    string `json:"audit_period_end"`