Merge branch 'master' into rescue
diff --git a/UPGRADING.md b/UPGRADING.md
index da3758b..a702cfc 100644
--- a/UPGRADING.md
+++ b/UPGRADING.md
@@ -238,7 +238,7 @@
   imageList, err := images.ExtractImages(page)
 
   for _, i := range imageList {
-    // "i" will be a images.Image
+    // "i" will be an images.Image
   }
 })
 ```
@@ -284,7 +284,7 @@
   imageList, err := images.ExtractImages(page)
 
   for _, i := range imageList {
-    // "i" will be a images.Image
+    // "i" will be an images.Image
   }
 })
 ```
diff --git a/acceptance/openstack/blockstorage/v1/snapshots_test.go b/acceptance/openstack/blockstorage/v1/snapshots_test.go
index 9132ee5..7741aa9 100644
--- a/acceptance/openstack/blockstorage/v1/snapshots_test.go
+++ b/acceptance/openstack/blockstorage/v1/snapshots_test.go
@@ -13,9 +13,9 @@
 
 func TestSnapshots(t *testing.T) {
 
-	client, err := newClient()
+	client, err := newClient(t)
 	th.AssertNoErr(t, err)
-	
+
 	v, err := volumes.Create(client, &volumes.CreateOpts{
 		Name: "gophercloud-test-volume",
 		Size: 1,
diff --git a/acceptance/openstack/blockstorage/v1/volumes_test.go b/acceptance/openstack/blockstorage/v1/volumes_test.go
index 99da39a..7760427 100644
--- a/acceptance/openstack/blockstorage/v1/volumes_test.go
+++ b/acceptance/openstack/blockstorage/v1/volumes_test.go
@@ -13,7 +13,7 @@
 	th "github.com/rackspace/gophercloud/testhelper"
 )
 
-func newClient() (*gophercloud.ServiceClient, error) {
+func newClient(t *testing.T) (*gophercloud.ServiceClient, error) {
 	ao, err := openstack.AuthOptionsFromEnv()
 	th.AssertNoErr(t, err)
 
@@ -26,7 +26,7 @@
 }
 
 func TestVolumes(t *testing.T) {
-	client, err := newClient()
+	client, err := newClient(t)
 	th.AssertNoErr(t, err)
 
 	cv, err := volumes.Create(client, &volumes.CreateOpts{
diff --git a/acceptance/openstack/blockstorage/v1/volumetypes_test.go b/acceptance/openstack/blockstorage/v1/volumetypes_test.go
index 5adcd81..000bc01 100644
--- a/acceptance/openstack/blockstorage/v1/volumetypes_test.go
+++ b/acceptance/openstack/blockstorage/v1/volumetypes_test.go
@@ -12,7 +12,7 @@
 )
 
 func TestVolumeTypes(t *testing.T) {
-	client, err := newClient()
+	client, err := newClient(t)
 	th.AssertNoErr(t, err)
 
 	vt, err := volumetypes.Create(client, &volumetypes.CreateOpts{
diff --git a/acceptance/openstack/compute/v2/bootfromvolume_test.go b/acceptance/openstack/compute/v2/bootfromvolume_test.go
index d08abe6..715af26 100644
--- a/acceptance/openstack/compute/v2/bootfromvolume_test.go
+++ b/acceptance/openstack/compute/v2/bootfromvolume_test.go
@@ -5,10 +5,10 @@
 import (
 	"testing"
 
+	"github.com/rackspace/gophercloud/acceptance/tools"
 	"github.com/rackspace/gophercloud/openstack/compute/v2/extensions/bootfromvolume"
 	"github.com/rackspace/gophercloud/openstack/compute/v2/servers"
 	th "github.com/rackspace/gophercloud/testhelper"
-	"github.com/smashwilson/gophercloud/acceptance/tools"
 )
 
 func TestBootFromVolume(t *testing.T) {
diff --git a/acceptance/openstack/compute/v2/servers_test.go b/acceptance/openstack/compute/v2/servers_test.go
index e223c18..be1fe7a 100644
--- a/acceptance/openstack/compute/v2/servers_test.go
+++ b/acceptance/openstack/compute/v2/servers_test.go
@@ -12,6 +12,7 @@
 	"github.com/rackspace/gophercloud/openstack/compute/v2/servers"
 	"github.com/rackspace/gophercloud/openstack/networking/v2/networks"
 	"github.com/rackspace/gophercloud/pagination"
+	th "github.com/rackspace/gophercloud/testhelper"
 )
 
 func TestListServers(t *testing.T) {
@@ -94,6 +95,8 @@
 	name := tools.RandomString("ACPTTEST", 16)
 	t.Logf("Attempting to create server: %s\n", name)
 
+	pwd := tools.MakeNewPassword("")
+
 	server, err := servers.Create(client, servers.CreateOpts{
 		Name:      name,
 		FlavorRef: choices.FlavorID,
@@ -101,11 +104,14 @@
 		Networks: []servers.Network{
 			servers.Network{UUID: network.ID},
 		},
+		AdminPass: pwd,
 	}).Extract()
 	if err != nil {
 		t.Fatalf("Unable to create server: %v", err)
 	}
 
+	th.AssertEquals(t, pwd, server.AdminPass)
+
 	return server, err
 }
 
diff --git a/acceptance/openstack/identity/v2/extension_test.go b/acceptance/openstack/identity/v2/extension_test.go
index 2b4e062..d1fa1e3 100644
--- a/acceptance/openstack/identity/v2/extension_test.go
+++ b/acceptance/openstack/identity/v2/extension_test.go
@@ -1,4 +1,4 @@
-// +build acceptance
+// +build acceptance identity
 
 package v2
 
diff --git a/acceptance/openstack/identity/v2/identity_test.go b/acceptance/openstack/identity/v2/identity_test.go
index feae233..96bf1fd 100644
--- a/acceptance/openstack/identity/v2/identity_test.go
+++ b/acceptance/openstack/identity/v2/identity_test.go
@@ -1,4 +1,4 @@
-// +build acceptance
+// +build acceptance identity
 
 package v2
 
diff --git a/acceptance/openstack/identity/v2/role_test.go b/acceptance/openstack/identity/v2/role_test.go
new file mode 100644
index 0000000..ba243fe
--- /dev/null
+++ b/acceptance/openstack/identity/v2/role_test.go
@@ -0,0 +1,58 @@
+// +build acceptance identity roles
+
+package v2
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/openstack/identity/v2/extensions/admin/roles"
+	"github.com/rackspace/gophercloud/pagination"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestRoles(t *testing.T) {
+	client := authenticatedClient(t)
+
+	tenantID := findTenant(t, client)
+	userID := createUser(t, client, tenantID)
+	roleID := listRoles(t, client)
+
+	addUserRole(t, client, tenantID, userID, roleID)
+
+	deleteUserRole(t, client, tenantID, userID, roleID)
+
+	deleteUser(t, client, userID)
+}
+
+func listRoles(t *testing.T, client *gophercloud.ServiceClient) string {
+	var roleID string
+
+	err := roles.List(client).EachPage(func(page pagination.Page) (bool, error) {
+		roleList, err := roles.ExtractRoles(page)
+		th.AssertNoErr(t, err)
+
+		for _, role := range roleList {
+			t.Logf("Listing role: ID [%s] Name [%s]", role.ID, role.Name)
+			roleID = role.ID
+		}
+
+		return true, nil
+	})
+
+	th.AssertNoErr(t, err)
+
+	return roleID
+}
+
+func addUserRole(t *testing.T, client *gophercloud.ServiceClient, tenantID, userID, roleID string) {
+	err := roles.AddUserRole(client, tenantID, userID, roleID).ExtractErr()
+	th.AssertNoErr(t, err)
+	t.Logf("Added role %s to user %s", roleID, userID)
+}
+
+func deleteUserRole(t *testing.T, client *gophercloud.ServiceClient, tenantID, userID, roleID string) {
+	err := roles.DeleteUserRole(client, tenantID, userID, roleID).ExtractErr()
+	th.AssertNoErr(t, err)
+	t.Logf("Removed role %s from user %s", roleID, userID)
+}
diff --git a/acceptance/openstack/identity/v2/tenant_test.go b/acceptance/openstack/identity/v2/tenant_test.go
index 2054598..578fc48 100644
--- a/acceptance/openstack/identity/v2/tenant_test.go
+++ b/acceptance/openstack/identity/v2/tenant_test.go
@@ -1,4 +1,4 @@
-// +build acceptance
+// +build acceptance identity
 
 package v2
 
diff --git a/acceptance/openstack/identity/v2/token_test.go b/acceptance/openstack/identity/v2/token_test.go
index 0632a48..d903140 100644
--- a/acceptance/openstack/identity/v2/token_test.go
+++ b/acceptance/openstack/identity/v2/token_test.go
@@ -1,4 +1,4 @@
-// +build acceptance
+// +build acceptance identity
 
 package v2
 
diff --git a/acceptance/openstack/identity/v2/user_test.go b/acceptance/openstack/identity/v2/user_test.go
new file mode 100644
index 0000000..fe73d19
--- /dev/null
+++ b/acceptance/openstack/identity/v2/user_test.go
@@ -0,0 +1,127 @@
+// +build acceptance identity
+
+package v2
+
+import (
+	"strconv"
+	"testing"
+
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/acceptance/tools"
+	"github.com/rackspace/gophercloud/openstack/identity/v2/tenants"
+	"github.com/rackspace/gophercloud/openstack/identity/v2/users"
+	"github.com/rackspace/gophercloud/pagination"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestUsers(t *testing.T) {
+	client := authenticatedClient(t)
+
+	tenantID := findTenant(t, client)
+
+	userID := createUser(t, client, tenantID)
+
+	listUsers(t, client)
+
+	getUser(t, client, userID)
+
+	updateUser(t, client, userID)
+
+	listUserRoles(t, client, tenantID, userID)
+
+	deleteUser(t, client, userID)
+}
+
+func findTenant(t *testing.T, client *gophercloud.ServiceClient) string {
+	var tenantID string
+	err := tenants.List(client, nil).EachPage(func(page pagination.Page) (bool, error) {
+		tenantList, err := tenants.ExtractTenants(page)
+		th.AssertNoErr(t, err)
+
+		for _, t := range tenantList {
+			tenantID = t.ID
+			break
+		}
+
+		return true, nil
+	})
+	th.AssertNoErr(t, err)
+
+	return tenantID
+}
+
+func createUser(t *testing.T, client *gophercloud.ServiceClient, tenantID string) string {
+	t.Log("Creating user")
+
+	opts := users.CreateOpts{
+		Name:     tools.RandomString("user_", 5),
+		Enabled:  users.Disabled,
+		TenantID: tenantID,
+		Email:    "new_user@foo.com",
+	}
+
+	user, err := users.Create(client, opts).Extract()
+	th.AssertNoErr(t, err)
+	t.Logf("Created user %s on tenant %s", user.ID, tenantID)
+
+	return user.ID
+}
+
+func listUsers(t *testing.T, client *gophercloud.ServiceClient) {
+	err := users.List(client).EachPage(func(page pagination.Page) (bool, error) {
+		userList, err := users.ExtractUsers(page)
+		th.AssertNoErr(t, err)
+
+		for _, user := range userList {
+			t.Logf("Listing user: ID [%s] Name [%s] Email [%s] Enabled? [%s]",
+				user.ID, user.Name, user.Email, strconv.FormatBool(user.Enabled))
+		}
+
+		return true, nil
+	})
+
+	th.AssertNoErr(t, err)
+}
+
+func getUser(t *testing.T, client *gophercloud.ServiceClient, userID string) {
+	_, err := users.Get(client, userID).Extract()
+	th.AssertNoErr(t, err)
+	t.Logf("Getting user %s", userID)
+}
+
+func updateUser(t *testing.T, client *gophercloud.ServiceClient, userID string) {
+	opts := users.UpdateOpts{Name: tools.RandomString("new_name", 5), Email: "new@foo.com"}
+	user, err := users.Update(client, userID, opts).Extract()
+	th.AssertNoErr(t, err)
+	t.Logf("Updated user %s: Name [%s] Email [%s]", userID, user.Name, user.Email)
+}
+
+func listUserRoles(t *testing.T, client *gophercloud.ServiceClient, tenantID, userID string) {
+	count := 0
+	err := users.ListRoles(client, tenantID, userID).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+
+		roleList, err := users.ExtractRoles(page)
+		th.AssertNoErr(t, err)
+
+		t.Logf("Listing roles for user %s", userID)
+
+		for _, r := range roleList {
+			t.Logf("- %s (%s)", r.Name, r.ID)
+		}
+
+		return true, nil
+	})
+
+	if count == 0 {
+		t.Logf("No roles for user %s", userID)
+	}
+
+	th.AssertNoErr(t, err)
+}
+
+func deleteUser(t *testing.T, client *gophercloud.ServiceClient, userID string) {
+	res := users.Delete(client, userID)
+	th.AssertNoErr(t, res.Err)
+	t.Logf("Deleted user %s", userID)
+}
diff --git a/acceptance/rackspace/compute/v2/bootfromvolume_test.go b/acceptance/rackspace/compute/v2/bootfromvolume_test.go
index 010bf42..c8c8e21 100644
--- a/acceptance/rackspace/compute/v2/bootfromvolume_test.go
+++ b/acceptance/rackspace/compute/v2/bootfromvolume_test.go
@@ -5,11 +5,11 @@
 import (
 	"testing"
 
+	"github.com/rackspace/gophercloud/acceptance/tools"
 	osBFV "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/bootfromvolume"
 	"github.com/rackspace/gophercloud/rackspace/compute/v2/bootfromvolume"
 	"github.com/rackspace/gophercloud/rackspace/compute/v2/servers"
 	th "github.com/rackspace/gophercloud/testhelper"
-	"github.com/smashwilson/gophercloud/acceptance/tools"
 )
 
 func TestBootFromVolume(t *testing.T) {
diff --git a/acceptance/rackspace/compute/v2/servers_test.go b/acceptance/rackspace/compute/v2/servers_test.go
index 511f0a9..81c8599 100644
--- a/acceptance/rackspace/compute/v2/servers_test.go
+++ b/acceptance/rackspace/compute/v2/servers_test.go
@@ -39,11 +39,14 @@
 
 	name := tools.RandomString("Gophercloud-", 8)
 
+	pwd := tools.MakeNewPassword("")
+
 	opts := &servers.CreateOpts{
 		Name:       name,
 		ImageRef:   options.imageID,
 		FlavorRef:  options.flavorID,
 		DiskConfig: diskconfig.Manual,
+		AdminPass:  pwd,
 	}
 
 	if keyName != "" {
@@ -59,6 +62,8 @@
 	th.AssertNoErr(t, err)
 	t.Logf("Server created successfully.")
 
+	th.CheckEquals(t, pwd, s.AdminPass)
+
 	return s
 }
 
@@ -120,7 +125,7 @@
 	original := server.AdminPass
 
 	t.Logf("Changing server password.")
-	err := servers.ChangeAdminPassword(client, server.ID, tools.MakeNewPassword(original)).Extract()
+	err := servers.ChangeAdminPassword(client, server.ID, tools.MakeNewPassword(original)).ExtractErr()
 	th.AssertNoErr(t, err)
 
 	err = servers.WaitForStatus(client, server.ID, "ACTIVE", 300)
@@ -131,7 +136,7 @@
 func rebootServer(t *testing.T, client *gophercloud.ServiceClient, server *os.Server) {
 	t.Logf("> servers.Reboot")
 
-	err := servers.Reboot(client, server.ID, os.HardReboot).Extract()
+	err := servers.Reboot(client, server.ID, os.HardReboot).ExtractErr()
 	th.AssertNoErr(t, err)
 
 	err = servers.WaitForStatus(client, server.ID, "ACTIVE", 300)
diff --git a/acceptance/rackspace/identity/v2/pkg.go b/acceptance/rackspace/identity/v2/pkg.go
new file mode 100644
index 0000000..5ec3cc8
--- /dev/null
+++ b/acceptance/rackspace/identity/v2/pkg.go
@@ -0,0 +1 @@
+package v2
diff --git a/acceptance/rackspace/identity/v2/role_test.go b/acceptance/rackspace/identity/v2/role_test.go
new file mode 100644
index 0000000..efaeb75
--- /dev/null
+++ b/acceptance/rackspace/identity/v2/role_test.go
@@ -0,0 +1,59 @@
+// +build acceptance identity roles
+
+package v2
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud"
+	os "github.com/rackspace/gophercloud/openstack/identity/v2/extensions/admin/roles"
+
+	"github.com/rackspace/gophercloud/pagination"
+	"github.com/rackspace/gophercloud/rackspace/identity/v2/roles"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestRoles(t *testing.T) {
+	client := authenticatedClient(t)
+
+	userID := createUser(t, client)
+	roleID := listRoles(t, client)
+
+	addUserRole(t, client, userID, roleID)
+
+	deleteUserRole(t, client, userID, roleID)
+
+	deleteUser(t, client, userID)
+}
+
+func listRoles(t *testing.T, client *gophercloud.ServiceClient) string {
+	var roleID string
+
+	err := roles.List(client).EachPage(func(page pagination.Page) (bool, error) {
+		roleList, err := os.ExtractRoles(page)
+		th.AssertNoErr(t, err)
+
+		for _, role := range roleList {
+			t.Logf("Listing role: ID [%s] Name [%s]", role.ID, role.Name)
+			roleID = role.ID
+		}
+
+		return true, nil
+	})
+
+	th.AssertNoErr(t, err)
+
+	return roleID
+}
+
+func addUserRole(t *testing.T, client *gophercloud.ServiceClient, userID, roleID string) {
+	err := roles.AddUserRole(client, userID, roleID).ExtractErr()
+	th.AssertNoErr(t, err)
+	t.Logf("Added role %s to user %s", roleID, userID)
+}
+
+func deleteUserRole(t *testing.T, client *gophercloud.ServiceClient, userID, roleID string) {
+	err := roles.DeleteUserRole(client, userID, roleID).ExtractErr()
+	th.AssertNoErr(t, err)
+	t.Logf("Removed role %s from user %s", roleID, userID)
+}
diff --git a/acceptance/rackspace/identity/v2/user_test.go b/acceptance/rackspace/identity/v2/user_test.go
new file mode 100644
index 0000000..28c0c83
--- /dev/null
+++ b/acceptance/rackspace/identity/v2/user_test.go
@@ -0,0 +1,93 @@
+// +build acceptance identity
+
+package v2
+
+import (
+	"strconv"
+	"testing"
+
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/acceptance/tools"
+	os "github.com/rackspace/gophercloud/openstack/identity/v2/users"
+	"github.com/rackspace/gophercloud/pagination"
+	"github.com/rackspace/gophercloud/rackspace/identity/v2/users"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestUsers(t *testing.T) {
+	client := authenticatedClient(t)
+
+	userID := createUser(t, client)
+
+	listUsers(t, client)
+
+	getUser(t, client, userID)
+
+	updateUser(t, client, userID)
+
+	resetApiKey(t, client, userID)
+
+	deleteUser(t, client, userID)
+}
+
+func createUser(t *testing.T, client *gophercloud.ServiceClient) string {
+	t.Log("Creating user")
+
+	opts := users.CreateOpts{
+		Username: tools.RandomString("user_", 5),
+		Enabled:  os.Disabled,
+		Email:    "new_user@foo.com",
+	}
+
+	user, err := users.Create(client, opts).Extract()
+	th.AssertNoErr(t, err)
+	t.Logf("Created user %s", user.ID)
+
+	return user.ID
+}
+
+func listUsers(t *testing.T, client *gophercloud.ServiceClient) {
+	err := users.List(client).EachPage(func(page pagination.Page) (bool, error) {
+		userList, err := os.ExtractUsers(page)
+		th.AssertNoErr(t, err)
+
+		for _, user := range userList {
+			t.Logf("Listing user: ID [%s] Username [%s] Email [%s] Enabled? [%s]",
+				user.ID, user.Username, user.Email, strconv.FormatBool(user.Enabled))
+		}
+
+		return true, nil
+	})
+
+	th.AssertNoErr(t, err)
+}
+
+func getUser(t *testing.T, client *gophercloud.ServiceClient, userID string) {
+	_, err := users.Get(client, userID).Extract()
+	th.AssertNoErr(t, err)
+	t.Logf("Getting user %s", userID)
+}
+
+func updateUser(t *testing.T, client *gophercloud.ServiceClient, userID string) {
+	opts := users.UpdateOpts{Username: tools.RandomString("new_name", 5), Email: "new@foo.com"}
+	user, err := users.Update(client, userID, opts).Extract()
+	th.AssertNoErr(t, err)
+	t.Logf("Updated user %s: Username [%s] Email [%s]", userID, user.Username, user.Email)
+}
+
+func deleteUser(t *testing.T, client *gophercloud.ServiceClient, userID string) {
+	res := users.Delete(client, userID)
+	th.AssertNoErr(t, res.Err)
+	t.Logf("Deleted user %s", userID)
+}
+
+func resetApiKey(t *testing.T, client *gophercloud.ServiceClient, userID string) {
+	key, err := users.ResetAPIKey(client, userID).Extract()
+	th.AssertNoErr(t, err)
+
+	if key.APIKey == "" {
+		t.Fatal("failed to reset API key for user")
+	}
+
+	t.Logf("Reset API key for user %s to %s", key.Username, key.APIKey)
+}
diff --git a/auth_options.go b/auth_options.go
index bc0ef65..19ce5d4 100644
--- a/auth_options.go
+++ b/auth_options.go
@@ -1,20 +1,28 @@
 package gophercloud
 
-// AuthOptions allows anyone calling Authenticate to supply the required access
-// credentials. Its fields are the union of those recognized by each identity
-// implementation and provider.
+/*
+AuthOptions stores information needed to authenticate to an OpenStack cluster.
+You can populate one manually, or use a provider's AuthOptionsFromEnv() function
+to read relevant information from the standard environment variables. Pass one
+to a provider's AuthenticatedClient function to authenticate and obtain a
+ProviderClient representing an active session on that provider.
+
+Its fields are the union of those recognized by each identity implementation and
+provider.
+*/
 type AuthOptions struct {
 	// IdentityEndpoint specifies the HTTP endpoint that is required to work with
-	// the Identity API of the appropriate version. Required by the identity
-	// services, but often populated by a provider Client.
+	// the Identity API of the appropriate version. While it's ultimately needed by
+	// all of the identity services, it will often be populated by a provider-level
+	// function.
 	IdentityEndpoint string
 
 	// Username is required if using Identity V2 API. Consult with your provider's
 	// control panel to discover your account's username. In Identity V3, either
-	// UserID or a combination of Username and DomainID or DomainName.
+	// UserID or a combination of Username and DomainID or DomainName are needed.
 	Username, UserID string
 
-	// Exactly one of Password or ApiKey is required for the Identity V2 and V3
+	// Exactly one of Password or APIKey is required for the Identity V2 and V3
 	// APIs. Consult with your provider's control panel to discover your account's
 	// preferred method of authentication.
 	Password, APIKey string
@@ -25,7 +33,7 @@
 
 	// The TenantID and TenantName fields are optional for the Identity V2 API.
 	// Some providers allow you to specify a TenantName instead of the TenantId.
-	// Some require both.  Your provider's authentication policies will determine
+	// Some require both. Your provider's authentication policies will determine
 	// how these fields influence authentication.
 	TenantID, TenantName string
 
@@ -34,5 +42,7 @@
 	// re-authenticate automatically if/when your token expires.  If you set it to
 	// false, it will not cache these settings, but re-authentication will not be
 	// possible.  This setting defaults to false.
+	//
+	// This setting is speculative and is currently not respected!
 	AllowReauth bool
 }
diff --git a/auth_results.go b/auth_results.go
index 1a1faa5..856a233 100644
--- a/auth_results.go
+++ b/auth_results.go
@@ -2,10 +2,9 @@
 
 import "time"
 
-// AuthResults encapsulates the raw results from an authentication request. As OpenStack allows
-// extensions to influence the structure returned in ways that Gophercloud cannot predict at
-// compile-time, you should use type-safe accessors to work with the data represented by this type,
-// such as ServiceCatalog and TokenID.
+// AuthResults [deprecated] is a leftover type from the v0.x days. It was
+// intended to describe common functionality among identity service results, but
+// is not actually used anywhere.
 type AuthResults interface {
 	// TokenID returns the token's ID value from the authentication response.
 	TokenID() (string, error)
diff --git a/doc.go b/doc.go
new file mode 100644
index 0000000..fb81a9d
--- /dev/null
+++ b/doc.go
@@ -0,0 +1,67 @@
+/*
+Package gophercloud provides a multi-vendor interface to OpenStack-compatible
+clouds. The library has a three-level hierarchy: providers, services, and
+resources.
+
+Provider structs represent the service providers that offer and manage a
+collection of services. Examples of providers include: OpenStack, Rackspace,
+HP. These are defined like so:
+
+  opts := gophercloud.AuthOptions{
+    IdentityEndpoint: "https://my-openstack.com:5000/v2.0",
+    Username: "{username}",
+    Password: "{password}",
+    TenantID: "{tenant_id}",
+  }
+
+  provider, err := openstack.AuthenticatedClient(opts)
+
+Service structs are specific to a provider and handle all of the logic and
+operations for a particular OpenStack service. Examples of services include:
+Compute, Object Storage, Block Storage. In order to define one, you need to
+pass in the parent provider, like so:
+
+  opts := gophercloud.EndpointOpts{Region: "RegionOne"}
+
+  client := openstack.NewComputeV2(provider, opts)
+
+Resource structs are the domain models that services make use of in order
+to work with and represent the state of API resources:
+
+  server, err := servers.Get(client, "{serverId}").Extract()
+
+Intermediate Result structs are returned for API operations, which allow
+generic access to the HTTP headers, response body, and any errors associated
+with the network transaction. To turn a result into a usable resource struct,
+you must call the Extract method which is chained to the response, or an
+Extract function from an applicable extension:
+
+  result := servers.Get(client, "{serverId}")
+
+  // Attempt to extract the disk configuration from the OS-DCF disk config
+  // extension:
+  config, err := diskconfig.ExtractGet(result)
+
+All requests that enumerate a collection return a Pager struct that is used to
+iterate through the results one page at a time. Use the EachPage method on that
+Pager to handle each successive Page in a closure, then use the appropriate
+extraction method from that request's package to interpret that Page as a slice
+of results:
+
+  err := servers.List(client, nil).EachPage(func (page pagination.Page) (bool, error) {
+    s, err := servers.ExtractServers(page)
+    if err != nil {
+      return false, err
+    }
+
+    // Handle the []servers.Server slice.
+
+    // Return "false" or an error to prematurely stop fetching new pages.
+    return true, nil
+  })
+
+This top-level package contains utility functions and data types that are used
+throughout the provider and service packages. Of particular note for end users
+are the AuthOptions and EndpointOpts structs.
+*/
+package gophercloud
diff --git a/endpoint_search.go b/endpoint_search.go
index b6f6b48..5189431 100644
--- a/endpoint_search.go
+++ b/endpoint_search.go
@@ -3,58 +3,85 @@
 import "errors"
 
 var (
-	// ErrServiceNotFound is returned when no service matches the EndpointOpts.
+	// ErrServiceNotFound is returned when no service in a service catalog matches
+	// the provided EndpointOpts. This is generally returned by provider service
+	// factory methods like "NewComputeV2()" and can mean that a service is not
+	// enabled for your account.
 	ErrServiceNotFound = errors.New("No suitable service could be found in the service catalog.")
 
-	// ErrEndpointNotFound is returned when no available endpoints match the provided EndpointOpts.
+	// ErrEndpointNotFound is returned when no available endpoints match the
+	// provided EndpointOpts. This is also generally returned by provider service
+	// factory methods, and usually indicates that a region was specified
+	// incorrectly.
 	ErrEndpointNotFound = errors.New("No suitable endpoint could be found in the service catalog.")
 )
 
-// Availability indicates whether a specific service endpoint is accessible.
-// Identity v2 lists these as different kinds of URLs ("adminURL",
-// "internalURL", and "publicURL"), while v3 lists them as "Interfaces".
+// Availability indicates to whom a specific service endpoint is accessible:
+// the internet at large, internal networks only, or only to administrators.
+// Different identity services use different terminology for these. Identity v2
+// lists them as different kinds of URLs within the service catalog ("adminURL",
+// "internalURL", and "publicURL"), while v3 lists them as "Interfaces" in an
+// endpoint's response.
 type Availability string
 
 const (
-	// AvailabilityAdmin makes an endpoint only available to administrators.
+	// AvailabilityAdmin indicates that an endpoint is only available to
+	// administrators.
 	AvailabilityAdmin Availability = "admin"
 
-	// AvailabilityPublic makes an endpoint available to everyone.
+	// AvailabilityPublic indicates that an endpoint is available to everyone on
+	// the internet.
 	AvailabilityPublic Availability = "public"
 
-	// AvailabilityInternal makes an endpoint only available within the cluster.
+	// AvailabilityInternal indicates that an endpoint is only available within
+	// the cluster's internal network.
 	AvailabilityInternal Availability = "internal"
 )
 
-// EndpointOpts contains options for finding an endpoint for an Openstack client.
+// EndpointOpts specifies search criteria used by queries against an
+// OpenStack service catalog. The options must contain enough information to
+// unambiguously identify one, and only one, endpoint within the catalog.
+//
+// Usually, these are passed to service client factory functions in a provider
+// package, like "rackspace.NewComputeV2()".
 type EndpointOpts struct {
-	// Type is the service type for the client (e.g., "compute", "object-store").
-	// Required.
+	// Type [required] is the service type for the client (e.g., "compute",
+	// "object-store"). Generally, this will be supplied by the service client
+	// function, but a user-given value will be honored if provided.
 	Type string
 
-	// Name is the service name for the client (e.g., "nova") as it appears in
-	// the service catalog. Services can have the same Type but a different Name,
-	// which is why both Type and Name are sometimes needed. Optional.
+	// Name [optional] is the service name for the client (e.g., "nova") as it
+	// appears in the service catalog. Services can have the same Type but a
+	// different Name, which is why both Type and Name are sometimes needed.
 	Name string
 
-	// Region is the geographic region in which the service resides. Required only
-	// for services that span multiple regions.
+	// Region [required] is the geographic region in which the endpoint resides,
+	// generally specifying which datacenter should house your resources.
+	// Required only for services that span multiple regions.
 	Region string
 
-	// Availability is the visibility of the endpoint to be returned. Valid types
-	// are: AvailabilityPublic, AvailabilityInternal, or AvailabilityAdmin.
-	// Availability is not required, and defaults to AvailabilityPublic.
-	// Not all providers or services offer all Availability options.
+	// Availability [optional] is the visibility of the endpoint to be returned.
+	// Valid types include the constants AvailabilityPublic, AvailabilityInternal,
+	// or AvailabilityAdmin from this package.
+	//
+	// Availability is not required, and defaults to AvailabilityPublic. Not all
+	// providers or services offer all Availability options.
 	Availability Availability
 }
 
-// EndpointLocator is a function that describes how to locate a single endpoint
-// from a service catalog for a specific ProviderClient. It should be set
-// during ProviderClient authentication and used to discover related ServiceClients.
+/*
+EndpointLocator is an internal function to be used by provider implementations.
+
+It provides an implementation that locates a single endpoint from a service
+catalog for a specific ProviderClient based on user-provided EndpointOpts. The
+provider then uses it to discover related ServiceClients.
+*/
 type EndpointLocator func(EndpointOpts) (string, error)
 
-// ApplyDefaults sets EndpointOpts fields if not already set. Currently,
-// EndpointOpts.Availability defaults to the public endpoint.
+// ApplyDefaults is an internal method to be used by provider implementations.
+//
+// It sets EndpointOpts fields if not already set, including a default type.
+// Currently, EndpointOpts.Availability defaults to the public endpoint.
 func (eo *EndpointOpts) ApplyDefaults(t string) {
 	if eo.Type == "" {
 		eo.Type = t
diff --git a/openstack/blockstorage/v1/snapshots/util_test.go b/openstack/blockstorage/v1/snapshots/util_test.go
deleted file mode 100644
index a4c4c82..0000000
--- a/openstack/blockstorage/v1/snapshots/util_test.go
+++ /dev/null
@@ -1,38 +0,0 @@
-package snapshots
-
-import (
-	"fmt"
-	"net/http"
-	"testing"
-	"time"
-
-	th "github.com/rackspace/gophercloud/testhelper"
-	"github.com/rackspace/gophercloud/testhelper/client"
-)
-
-func TestWaitForStatus(t *testing.T) {
-	th.SetupHTTP()
-	defer th.TeardownHTTP()
-
-	th.Mux.HandleFunc("/snapshots/1234", func(w http.ResponseWriter, r *http.Request) {
-		time.Sleep(2 * time.Second)
-		w.Header().Add("Content-Type", "application/json")
-		w.WriteHeader(http.StatusOK)
-		fmt.Fprintf(w, `
-		{
-			"snapshot": {
-				"display_name": "snapshot-001",
-				"id": "1234",
-				"status":"available"
-			}
-		}`)
-	})
-
-	err := WaitForStatus(client.ServiceClient(), "1234", "available", 0)
-	if err == nil {
-		t.Errorf("Expected error: 'Time Out in WaitFor'")
-	}
-
-	err = WaitForStatus(client.ServiceClient(), "1234", "available", 3)
-	th.CheckNoErr(t, err)
-}
diff --git a/openstack/blockstorage/v1/volumes/util_test.go b/openstack/blockstorage/v1/volumes/util_test.go
deleted file mode 100644
index 24ef3b6..0000000
--- a/openstack/blockstorage/v1/volumes/util_test.go
+++ /dev/null
@@ -1,38 +0,0 @@
-package volumes
-
-import (
-	"fmt"
-	"net/http"
-	"testing"
-	"time"
-
-	th "github.com/rackspace/gophercloud/testhelper"
-	"github.com/rackspace/gophercloud/testhelper/client"
-)
-
-func TestWaitForStatus(t *testing.T) {
-	th.SetupHTTP()
-	defer th.TeardownHTTP()
-
-	th.Mux.HandleFunc("/volumes/1234", func(w http.ResponseWriter, r *http.Request) {
-		time.Sleep(3 * time.Second)
-		w.Header().Add("Content-Type", "application/json")
-		w.WriteHeader(http.StatusOK)
-		fmt.Fprintf(w, `
-		{
-			"volume": {
-				"display_name": "vol-001",
-				"id": "1234",
-				"status":"available"
-			}
-		}`)
-	})
-
-	err := WaitForStatus(client.ServiceClient(), "1234", "available", 0)
-	if err == nil {
-		t.Errorf("Expected error: 'Time Out in WaitFor'")
-	}
-
-	err = WaitForStatus(client.ServiceClient(), "1234", "available", 6)
-	th.CheckNoErr(t, err)
-}
diff --git a/openstack/compute/v2/servers/requests.go b/openstack/compute/v2/servers/requests.go
index 96e1f11..8182c72 100644
--- a/openstack/compute/v2/servers/requests.go
+++ b/openstack/compute/v2/servers/requests.go
@@ -131,6 +131,10 @@
 
 	// ConfigDrive [optional] enables metadata injection through a configuration drive.
 	ConfigDrive bool
+
+	// AdminPass [optional] sets the root user password. If not set, a randomly-generated
+	// password will be created and returned in the response.
+	AdminPass string
 }
 
 // ToServerCreateMap assembles a request body based on the contents of a CreateOpts.
@@ -158,6 +162,9 @@
 	if opts.Metadata != nil {
 		server["metadata"] = opts.Metadata
 	}
+	if opts.AdminPass != "" {
+		server["adminPass"] = opts.AdminPass
+	}
 
 	if len(opts.SecurityGroups) > 0 {
 		securityGroups := make([]map[string]interface{}, len(opts.SecurityGroups))
diff --git a/openstack/compute/v2/servers/util_test.go b/openstack/compute/v2/servers/util_test.go
deleted file mode 100644
index e192ae3..0000000
--- a/openstack/compute/v2/servers/util_test.go
+++ /dev/null
@@ -1,38 +0,0 @@
-package servers
-
-import (
-	"fmt"
-	"net/http"
-	"testing"
-	"time"
-
-	th "github.com/rackspace/gophercloud/testhelper"
-	"github.com/rackspace/gophercloud/testhelper/client"
-)
-
-func TestWaitForStatus(t *testing.T) {
-	th.SetupHTTP()
-	defer th.TeardownHTTP()
-
-	th.Mux.HandleFunc("/servers/4321", func(w http.ResponseWriter, r *http.Request) {
-		time.Sleep(2 * time.Second)
-		w.Header().Add("Content-Type", "application/json")
-		w.WriteHeader(http.StatusOK)
-		fmt.Fprintf(w, `
-		{
-			"server": {
-				"name": "the-server",
-				"id": "4321",
-				"status": "ACTIVE"
-			}
-		}`)
-	})
-
-	err := WaitForStatus(client.ServiceClient(), "4321", "ACTIVE", 0)
-	if err == nil {
-		t.Errorf("Expected error: 'Time Out in WaitFor'")
-	}
-
-	err = WaitForStatus(client.ServiceClient(), "4321", "ACTIVE", 3)
-	th.CheckNoErr(t, err)
-}
diff --git a/openstack/identity/v2/extensions/admin/roles/docs.go b/openstack/identity/v2/extensions/admin/roles/docs.go
new file mode 100644
index 0000000..8954178
--- /dev/null
+++ b/openstack/identity/v2/extensions/admin/roles/docs.go
@@ -0,0 +1,16 @@
+// Package roles provides functionality to interact with and control roles on
+// the API.
+//
+// A role represents a personality that a user can assume when performing a
+// specific set of operations. If a role includes a set of rights and
+// privileges, a user assuming that role inherits those rights and privileges.
+//
+// When a token is generated, the list of roles that user can assume is returned
+// back to them. Services that are being called by that user determine how they
+// interpret the set of roles a user has and to which operations or resources
+// each role grants access.
+//
+// It is up to individual services such as Compute or Image to assign meaning
+// to these roles. As far as the Identity service is concerned, a role is an
+// arbitrary name assigned by the user.
+package roles
diff --git a/openstack/identity/v2/extensions/admin/roles/fixtures.go b/openstack/identity/v2/extensions/admin/roles/fixtures.go
new file mode 100644
index 0000000..8256f0f
--- /dev/null
+++ b/openstack/identity/v2/extensions/admin/roles/fixtures.go
@@ -0,0 +1,48 @@
+package roles
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	th "github.com/rackspace/gophercloud/testhelper"
+	fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func MockListRoleResponse(t *testing.T) {
+	th.Mux.HandleFunc("/OS-KSADM/roles", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusOK)
+
+		fmt.Fprintf(w, `
+{
+    "roles": [
+        {
+            "id": "123",
+            "name": "compute:admin",
+            "description": "Nova Administrator"
+        }
+    ]
+}
+  `)
+	})
+}
+
+func MockAddUserRoleResponse(t *testing.T) {
+	th.Mux.HandleFunc("/tenants/{tenant_id}/users/{user_id}/roles/OS-KSADM/{role_id}", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "PUT")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+		w.WriteHeader(http.StatusCreated)
+	})
+}
+
+func MockDeleteUserRoleResponse(t *testing.T) {
+	th.Mux.HandleFunc("/tenants/{tenant_id}/users/{user_id}/roles/OS-KSADM/{role_id}", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "DELETE")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+		w.WriteHeader(http.StatusNoContent)
+	})
+}
diff --git a/openstack/identity/v2/extensions/admin/roles/requests.go b/openstack/identity/v2/extensions/admin/roles/requests.go
new file mode 100644
index 0000000..152031a
--- /dev/null
+++ b/openstack/identity/v2/extensions/admin/roles/requests.go
@@ -0,0 +1,44 @@
+package roles
+
+import (
+	"github.com/racker/perigee"
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// List is the operation responsible for listing all available global roles
+// that a user can adopt.
+func List(client *gophercloud.ServiceClient) pagination.Pager {
+	createPage := func(r pagination.PageResult) pagination.Page {
+		return RolePage{pagination.SinglePageBase(r)}
+	}
+	return pagination.NewPager(client, rootURL(client), createPage)
+}
+
+// AddUserRole is the operation responsible for assigning a particular role to
+// a user. This is confined to the scope of the user's tenant - so the tenant
+// ID is a required argument.
+func AddUserRole(client *gophercloud.ServiceClient, tenantID, userID, roleID string) UserRoleResult {
+	var result UserRoleResult
+
+	_, result.Err = perigee.Request("PUT", userRoleURL(client, tenantID, userID, roleID), perigee.Options{
+		MoreHeaders: client.AuthenticatedHeaders(),
+		OkCodes:     []int{200, 201},
+	})
+
+	return result
+}
+
+// DeleteUserRole is the operation responsible for deleting a particular role
+// from a user. This is confined to the scope of the user's tenant - so the
+// tenant ID is a required argument.
+func DeleteUserRole(client *gophercloud.ServiceClient, tenantID, userID, roleID string) UserRoleResult {
+	var result UserRoleResult
+
+	_, result.Err = perigee.Request("DELETE", userRoleURL(client, tenantID, userID, roleID), perigee.Options{
+		MoreHeaders: client.AuthenticatedHeaders(),
+		OkCodes:     []int{204},
+	})
+
+	return result
+}
diff --git a/openstack/identity/v2/extensions/admin/roles/requests_test.go b/openstack/identity/v2/extensions/admin/roles/requests_test.go
new file mode 100644
index 0000000..7bfeea4
--- /dev/null
+++ b/openstack/identity/v2/extensions/admin/roles/requests_test.go
@@ -0,0 +1,64 @@
+package roles
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud/pagination"
+	th "github.com/rackspace/gophercloud/testhelper"
+	"github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestRole(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	MockListRoleResponse(t)
+
+	count := 0
+
+	err := List(client.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := ExtractRoles(page)
+		if err != nil {
+			t.Errorf("Failed to extract users: %v", err)
+			return false, err
+		}
+
+		expected := []Role{
+			Role{
+				ID:          "123",
+				Name:        "compute:admin",
+				Description: "Nova Administrator",
+			},
+		}
+
+		th.CheckDeepEquals(t, expected, actual)
+
+		return true, nil
+	})
+
+	th.AssertNoErr(t, err)
+	th.AssertEquals(t, 1, count)
+}
+
+func TestAddUserRole(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	MockAddUserRoleResponse(t)
+
+	err := AddUserRole(client.ServiceClient(), "{tenant_id}", "{user_id}", "{role_id}").ExtractErr()
+
+	th.AssertNoErr(t, err)
+}
+
+func TestDeleteUserRole(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	MockDeleteUserRoleResponse(t)
+
+	err := DeleteUserRole(client.ServiceClient(), "{tenant_id}", "{user_id}", "{role_id}").ExtractErr()
+
+	th.AssertNoErr(t, err)
+}
diff --git a/openstack/identity/v2/extensions/admin/roles/results.go b/openstack/identity/v2/extensions/admin/roles/results.go
new file mode 100644
index 0000000..ebb3aa5
--- /dev/null
+++ b/openstack/identity/v2/extensions/admin/roles/results.go
@@ -0,0 +1,53 @@
+package roles
+
+import (
+	"github.com/mitchellh/mapstructure"
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// Role represents an API role resource.
+type Role struct {
+	// The unique ID for the role.
+	ID string
+
+	// The human-readable name of the role.
+	Name string
+
+	// The description of the role.
+	Description string
+
+	// The associated service for this role.
+	ServiceID string
+}
+
+// RolePage is a single page of a user Role collection.
+type RolePage struct {
+	pagination.SinglePageBase
+}
+
+// IsEmpty determines whether or not a page of Tenants contains any results.
+func (page RolePage) IsEmpty() (bool, error) {
+	users, err := ExtractRoles(page)
+	if err != nil {
+		return false, err
+	}
+	return len(users) == 0, nil
+}
+
+// ExtractRoles returns a slice of roles contained in a single page of results.
+func ExtractRoles(page pagination.Page) ([]Role, error) {
+	casted := page.(RolePage).Body
+	var response struct {
+		Roles []Role `mapstructure:"roles"`
+	}
+
+	err := mapstructure.Decode(casted, &response)
+	return response.Roles, err
+}
+
+// UserRoleResult represents the result of either an AddUserRole or
+// a DeleteUserRole operation.
+type UserRoleResult struct {
+	gophercloud.ErrResult
+}
diff --git a/openstack/identity/v2/extensions/admin/roles/urls.go b/openstack/identity/v2/extensions/admin/roles/urls.go
new file mode 100644
index 0000000..61b3155
--- /dev/null
+++ b/openstack/identity/v2/extensions/admin/roles/urls.go
@@ -0,0 +1,21 @@
+package roles
+
+import "github.com/rackspace/gophercloud"
+
+const (
+	ExtPath  = "OS-KSADM"
+	RolePath = "roles"
+	UserPath = "users"
+)
+
+func resourceURL(c *gophercloud.ServiceClient, id string) string {
+	return c.ServiceURL(ExtPath, RolePath, id)
+}
+
+func rootURL(c *gophercloud.ServiceClient) string {
+	return c.ServiceURL(ExtPath, RolePath)
+}
+
+func userRoleURL(c *gophercloud.ServiceClient, tenantID, userID, roleID string) string {
+	return c.ServiceURL("tenants", tenantID, UserPath, userID, RolePath, ExtPath, roleID)
+}
diff --git a/openstack/identity/v2/users/doc.go b/openstack/identity/v2/users/doc.go
new file mode 100644
index 0000000..82abcb9
--- /dev/null
+++ b/openstack/identity/v2/users/doc.go
@@ -0,0 +1 @@
+package users
diff --git a/openstack/identity/v2/users/fixtures.go b/openstack/identity/v2/users/fixtures.go
new file mode 100644
index 0000000..8941868
--- /dev/null
+++ b/openstack/identity/v2/users/fixtures.go
@@ -0,0 +1,163 @@
+package users
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	th "github.com/rackspace/gophercloud/testhelper"
+	fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func MockListUserResponse(t *testing.T) {
+	th.Mux.HandleFunc("/users", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusOK)
+
+		fmt.Fprintf(w, `
+{
+    "users":[
+        {
+            "id": "u1000",
+						"name": "John Smith",
+            "username": "jqsmith",
+            "email": "john.smith@example.org",
+            "enabled": true,
+						"tenant_id": "12345"
+        },
+        {
+            "id": "u1001",
+						"name": "Jane Smith",
+            "username": "jqsmith",
+            "email": "jane.smith@example.org",
+            "enabled": true,
+						"tenant_id": "12345"
+        }
+    ]
+}
+  `)
+	})
+}
+
+func mockCreateUserResponse(t *testing.T) {
+	th.Mux.HandleFunc("/users", 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, `
+{
+    "user": {
+		    "name": "new_user",
+		    "tenant_id": "12345",
+				"enabled": false,
+				"email": "new_user@foo.com"
+    }
+}
+	`)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusOK)
+
+		fmt.Fprintf(w, `
+{
+    "user": {
+        "name": "new_user",
+        "tenant_id": "12345",
+        "enabled": false,
+        "email": "new_user@foo.com",
+        "id": "c39e3de9be2d4c779f1dfd6abacc176d"
+    }
+}
+`)
+	})
+}
+
+func mockGetUserResponse(t *testing.T) {
+	th.Mux.HandleFunc("/users/new_user", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusOK)
+
+		fmt.Fprintf(w, `
+{
+		"user": {
+				"name": "new_user",
+				"tenant_id": "12345",
+				"enabled": false,
+				"email": "new_user@foo.com",
+				"id": "c39e3de9be2d4c779f1dfd6abacc176d"
+		}
+}
+`)
+	})
+}
+
+func mockUpdateUserResponse(t *testing.T) {
+	th.Mux.HandleFunc("/users/c39e3de9be2d4c779f1dfd6abacc176d", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "PUT")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+		th.TestJSONRequest(t, r, `
+{
+    "user": {
+		    "name": "new_name",
+		    "enabled": true,
+		    "email": "new_email@foo.com"
+    }
+}
+`)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusOK)
+
+		fmt.Fprintf(w, `
+{
+		"user": {
+				"name": "new_name",
+				"tenant_id": "12345",
+				"enabled": true,
+				"email": "new_email@foo.com",
+				"id": "c39e3de9be2d4c779f1dfd6abacc176d"
+		}
+}
+`)
+	})
+}
+
+func mockDeleteUserResponse(t *testing.T) {
+	th.Mux.HandleFunc("/users/c39e3de9be2d4c779f1dfd6abacc176d", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "DELETE")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+		w.WriteHeader(http.StatusNoContent)
+	})
+}
+
+func mockListRolesResponse(t *testing.T) {
+	th.Mux.HandleFunc("/tenants/1d8b6120dcc640fda4fc9194ffc80273/users/c39e3de9be2d4c779f1dfd6abacc176d/roles", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusOK)
+
+		fmt.Fprintf(w, `
+{
+    "roles": [
+        {
+            "id": "9fe2ff9ee4384b1894a90878d3e92bab",
+            "name": "foo_role"
+        },
+        {
+            "id": "1ea3d56793574b668e85960fbf651e13",
+            "name": "admin"
+        }
+    ]
+}
+	`)
+	})
+}
diff --git a/openstack/identity/v2/users/requests.go b/openstack/identity/v2/users/requests.go
new file mode 100644
index 0000000..4ce395f
--- /dev/null
+++ b/openstack/identity/v2/users/requests.go
@@ -0,0 +1,180 @@
+package users
+
+import (
+	"errors"
+
+	"github.com/racker/perigee"
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+func List(client *gophercloud.ServiceClient) pagination.Pager {
+	createPage := func(r pagination.PageResult) pagination.Page {
+		return UserPage{pagination.SinglePageBase(r)}
+	}
+
+	return pagination.NewPager(client, rootURL(client), createPage)
+}
+
+// EnabledState represents whether the user is enabled or not.
+type EnabledState *bool
+
+// Useful variables to use when creating or updating users.
+var (
+	iTrue  = true
+	iFalse = false
+
+	Enabled  EnabledState = &iTrue
+	Disabled EnabledState = &iFalse
+)
+
+// CommonOpts are the parameters that are shared between CreateOpts and
+// UpdateOpts
+type CommonOpts struct {
+	// Either a name or username is required. When provided, the value must be
+	// unique or a 409 conflict error will be returned. If you provide a name but
+	// omit a username, the latter will be set to the former; and vice versa.
+	Name, Username string
+
+	// The ID of the tenant to which you want to assign this user.
+	TenantID string
+
+	// Indicates whether this user is enabled or not.
+	Enabled EnabledState
+
+	// The email address of this user.
+	Email string
+}
+
+// CreateOpts represents the options needed when creating new users.
+type CreateOpts CommonOpts
+
+// CreateOptsBuilder describes struct types that can be accepted by the Create call.
+type CreateOptsBuilder interface {
+	ToUserCreateMap() (map[string]interface{}, error)
+}
+
+// ToUserCreateMap assembles a request body based on the contents of a CreateOpts.
+func (opts CreateOpts) ToUserCreateMap() (map[string]interface{}, error) {
+	m := make(map[string]interface{})
+
+	if opts.Name == "" && opts.Username == "" {
+		return m, errors.New("Either a Name or Username must be provided")
+	}
+
+	if opts.Name != "" {
+		m["name"] = opts.Name
+	}
+	if opts.Username != "" {
+		m["username"] = opts.Username
+	}
+	if opts.Enabled != nil {
+		m["enabled"] = &opts.Enabled
+	}
+	if opts.Email != "" {
+		m["email"] = opts.Email
+	}
+	if opts.TenantID != "" {
+		m["tenant_id"] = opts.TenantID
+	}
+
+	return map[string]interface{}{"user": m}, nil
+}
+
+// Create is the operation responsible for creating new users.
+func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) CreateResult {
+	var res CreateResult
+
+	reqBody, err := opts.ToUserCreateMap()
+	if err != nil {
+		res.Err = err
+		return res
+	}
+
+	_, res.Err = perigee.Request("POST", rootURL(client), perigee.Options{
+		Results:     &res.Body,
+		ReqBody:     reqBody,
+		MoreHeaders: client.AuthenticatedHeaders(),
+		OkCodes:     []int{200, 201},
+	})
+
+	return res
+}
+
+// Get requests details on a single user, either by ID.
+func Get(client *gophercloud.ServiceClient, id string) GetResult {
+	var result GetResult
+
+	_, result.Err = perigee.Request("GET", ResourceURL(client, id), perigee.Options{
+		Results:     &result.Body,
+		MoreHeaders: client.AuthenticatedHeaders(),
+		OkCodes:     []int{200},
+	})
+
+	return result
+}
+
+// UpdateOptsBuilder allows extensions to add additional attributes to the Update request.
+type UpdateOptsBuilder interface {
+	ToUserUpdateMap() map[string]interface{}
+}
+
+// UpdateOpts specifies the base attributes that may be updated on an existing server.
+type UpdateOpts CommonOpts
+
+// ToUserUpdateMap formats an UpdateOpts structure into a request body.
+func (opts UpdateOpts) ToUserUpdateMap() map[string]interface{} {
+	m := make(map[string]interface{})
+
+	if opts.Name != "" {
+		m["name"] = opts.Name
+	}
+	if opts.Username != "" {
+		m["username"] = opts.Username
+	}
+	if opts.Enabled != nil {
+		m["enabled"] = &opts.Enabled
+	}
+	if opts.Email != "" {
+		m["email"] = opts.Email
+	}
+	if opts.TenantID != "" {
+		m["tenant_id"] = opts.TenantID
+	}
+
+	return map[string]interface{}{"user": m}
+}
+
+// Update is the operation responsible for updating exist users by their UUID.
+func Update(client *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) UpdateResult {
+	var result UpdateResult
+
+	_, result.Err = perigee.Request("PUT", ResourceURL(client, id), perigee.Options{
+		Results:     &result.Body,
+		ReqBody:     opts.ToUserUpdateMap(),
+		MoreHeaders: client.AuthenticatedHeaders(),
+		OkCodes:     []int{200},
+	})
+
+	return result
+}
+
+// Delete is the operation responsible for permanently deleting an API user.
+func Delete(client *gophercloud.ServiceClient, id string) DeleteResult {
+	var result DeleteResult
+
+	_, result.Err = perigee.Request("DELETE", ResourceURL(client, id), perigee.Options{
+		MoreHeaders: client.AuthenticatedHeaders(),
+		OkCodes:     []int{204},
+	})
+
+	return result
+}
+
+func ListRoles(client *gophercloud.ServiceClient, tenantID, userID string) pagination.Pager {
+	createPage := func(r pagination.PageResult) pagination.Page {
+		return RolePage{pagination.SinglePageBase(r)}
+	}
+
+	return pagination.NewPager(client, listRolesURL(client, tenantID, userID), createPage)
+}
diff --git a/openstack/identity/v2/users/requests_test.go b/openstack/identity/v2/users/requests_test.go
new file mode 100644
index 0000000..04f8371
--- /dev/null
+++ b/openstack/identity/v2/users/requests_test.go
@@ -0,0 +1,165 @@
+package users
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud/pagination"
+	th "github.com/rackspace/gophercloud/testhelper"
+	"github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestList(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	MockListUserResponse(t)
+
+	count := 0
+
+	err := List(client.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := ExtractUsers(page)
+		if err != nil {
+			t.Errorf("Failed to extract users: %v", err)
+			return false, err
+		}
+
+		expected := []User{
+			User{
+				ID:       "u1000",
+				Name:     "John Smith",
+				Username: "jqsmith",
+				Email:    "john.smith@example.org",
+				Enabled:  true,
+				TenantID: "12345",
+			},
+			User{
+				ID:       "u1001",
+				Name:     "Jane Smith",
+				Username: "jqsmith",
+				Email:    "jane.smith@example.org",
+				Enabled:  true,
+				TenantID: "12345",
+			},
+		}
+
+		th.CheckDeepEquals(t, expected, actual)
+
+		return true, nil
+	})
+
+	th.AssertNoErr(t, err)
+	th.AssertEquals(t, 1, count)
+}
+
+func TestCreateUser(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	mockCreateUserResponse(t)
+
+	opts := CreateOpts{
+		Name:     "new_user",
+		TenantID: "12345",
+		Enabled:  Disabled,
+		Email:    "new_user@foo.com",
+	}
+
+	user, err := Create(client.ServiceClient(), opts).Extract()
+
+	th.AssertNoErr(t, err)
+
+	expected := &User{
+		Name:     "new_user",
+		ID:       "c39e3de9be2d4c779f1dfd6abacc176d",
+		Email:    "new_user@foo.com",
+		Enabled:  false,
+		TenantID: "12345",
+	}
+
+	th.AssertDeepEquals(t, expected, user)
+}
+
+func TestGetUser(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	mockGetUserResponse(t)
+
+	user, err := Get(client.ServiceClient(), "new_user").Extract()
+	th.AssertNoErr(t, err)
+
+	expected := &User{
+		Name:     "new_user",
+		ID:       "c39e3de9be2d4c779f1dfd6abacc176d",
+		Email:    "new_user@foo.com",
+		Enabled:  false,
+		TenantID: "12345",
+	}
+
+	th.AssertDeepEquals(t, expected, user)
+}
+
+func TestUpdateUser(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	mockUpdateUserResponse(t)
+
+	id := "c39e3de9be2d4c779f1dfd6abacc176d"
+	opts := UpdateOpts{
+		Name:    "new_name",
+		Enabled: Enabled,
+		Email:   "new_email@foo.com",
+	}
+
+	user, err := Update(client.ServiceClient(), id, opts).Extract()
+
+	th.AssertNoErr(t, err)
+
+	expected := &User{
+		Name:     "new_name",
+		ID:       id,
+		Email:    "new_email@foo.com",
+		Enabled:  true,
+		TenantID: "12345",
+	}
+
+	th.AssertDeepEquals(t, expected, user)
+}
+
+func TestDeleteUser(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	mockDeleteUserResponse(t)
+
+	res := Delete(client.ServiceClient(), "c39e3de9be2d4c779f1dfd6abacc176d")
+	th.AssertNoErr(t, res.Err)
+}
+
+func TestListingUserRoles(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	mockListRolesResponse(t)
+
+	tenantID := "1d8b6120dcc640fda4fc9194ffc80273"
+	userID := "c39e3de9be2d4c779f1dfd6abacc176d"
+
+	err := ListRoles(client.ServiceClient(), tenantID, userID).EachPage(func(page pagination.Page) (bool, error) {
+		actual, err := ExtractRoles(page)
+		th.AssertNoErr(t, err)
+
+		expected := []Role{
+			Role{ID: "9fe2ff9ee4384b1894a90878d3e92bab", Name: "foo_role"},
+			Role{ID: "1ea3d56793574b668e85960fbf651e13", Name: "admin"},
+		}
+
+		th.CheckDeepEquals(t, expected, actual)
+
+		return true, nil
+	})
+
+	th.AssertNoErr(t, err)
+}
diff --git a/openstack/identity/v2/users/results.go b/openstack/identity/v2/users/results.go
new file mode 100644
index 0000000..f531d5d
--- /dev/null
+++ b/openstack/identity/v2/users/results.go
@@ -0,0 +1,128 @@
+package users
+
+import (
+	"github.com/mitchellh/mapstructure"
+
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// User represents a user resource that exists on the API.
+type User struct {
+	// The UUID for this user.
+	ID string
+
+	// The human name for this user.
+	Name string
+
+	// The username for this user.
+	Username string
+
+	// Indicates whether the user is enabled (true) or disabled (false).
+	Enabled bool
+
+	// The email address for this user.
+	Email string
+
+	// The ID of the tenant to which this user belongs.
+	TenantID string `mapstructure:"tenant_id"`
+}
+
+// Role assigns specific responsibilities to users, allowing them to accomplish
+// certain API operations whilst scoped to a service.
+type Role struct {
+	// UUID of the role
+	ID string
+
+	// Name of the role
+	Name string
+}
+
+// UserPage is a single page of a User collection.
+type UserPage struct {
+	pagination.SinglePageBase
+}
+
+// RolePage is a single page of a user Role collection.
+type RolePage struct {
+	pagination.SinglePageBase
+}
+
+// IsEmpty determines whether or not a page of Tenants contains any results.
+func (page UserPage) IsEmpty() (bool, error) {
+	users, err := ExtractUsers(page)
+	if err != nil {
+		return false, err
+	}
+	return len(users) == 0, nil
+}
+
+// ExtractUsers returns a slice of Tenants contained in a single page of results.
+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
+}
+
+// IsEmpty determines whether or not a page of Tenants contains any results.
+func (page RolePage) IsEmpty() (bool, error) {
+	users, err := ExtractRoles(page)
+	if err != nil {
+		return false, err
+	}
+	return len(users) == 0, nil
+}
+
+// ExtractRoles returns a slice of Roles contained in a single page of results.
+func ExtractRoles(page pagination.Page) ([]Role, error) {
+	casted := page.(RolePage).Body
+	var response struct {
+		Roles []Role `mapstructure:"roles"`
+	}
+
+	err := mapstructure.Decode(casted, &response)
+	return response.Roles, err
+}
+
+type commonResult struct {
+	gophercloud.Result
+}
+
+// Extract interprets any commonResult as a User, if possible.
+func (r commonResult) 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
+}
+
+// CreateResult represents the result of a Create operation
+type CreateResult struct {
+	commonResult
+}
+
+// GetResult represents the result of a Get operation
+type GetResult struct {
+	commonResult
+}
+
+// UpdateResult represents the result of an Update operation
+type UpdateResult struct {
+	commonResult
+}
+
+// DeleteResult represents the result of a Delete operation
+type DeleteResult struct {
+	commonResult
+}
diff --git a/openstack/identity/v2/users/urls.go b/openstack/identity/v2/users/urls.go
new file mode 100644
index 0000000..7ec4385
--- /dev/null
+++ b/openstack/identity/v2/users/urls.go
@@ -0,0 +1,21 @@
+package users
+
+import "github.com/rackspace/gophercloud"
+
+const (
+	tenantPath = "tenants"
+	userPath   = "users"
+	rolePath   = "roles"
+)
+
+func ResourceURL(c *gophercloud.ServiceClient, id string) string {
+	return c.ServiceURL(userPath, id)
+}
+
+func rootURL(c *gophercloud.ServiceClient) string {
+	return c.ServiceURL(userPath)
+}
+
+func listRolesURL(c *gophercloud.ServiceClient, tenantID, userID string) string {
+	return c.ServiceURL(tenantPath, tenantID, userPath, userID, rolePath)
+}
diff --git a/openstack/networking/v2/ports/requests.go b/openstack/networking/v2/ports/requests.go
index 3399907..06d273e 100644
--- a/openstack/networking/v2/ports/requests.go
+++ b/openstack/networking/v2/ports/requests.go
@@ -164,7 +164,6 @@
 		ReqBody:     &reqBody,
 		Results:     &res.Body,
 		OkCodes:     []int{201},
-		DumpReqJson: true,
 	})
 
 	return res
diff --git a/openstack/objectstorage/v1/objects/requests.go b/openstack/objectstorage/v1/objects/requests.go
index 9778de3..7b96fa2 100644
--- a/openstack/objectstorage/v1/objects/requests.go
+++ b/openstack/objectstorage/v1/objects/requests.go
@@ -18,6 +18,10 @@
 
 // ListOpts is a structure that holds parameters for listing objects.
 type ListOpts struct {
+	// Full is a true/false value that represents the amount of object information
+	// returned. If Full is set to true, then the content-type, number of bytes, hash
+	// date last modified, and name are returned. If set to false or not set, then
+	// only the object names are returned.
 	Full      bool
 	Limit     int    `q:"limit"`
 	Marker    string `q:"marker"`
@@ -88,7 +92,7 @@
 
 // ToObjectDownloadParams formats a DownloadOpts into a query string and map of
 // headers.
-func (opts ListOpts) ToObjectDownloadParams() (map[string]string, string, error) {
+func (opts DownloadOpts) ToObjectDownloadParams() (map[string]string, string, error) {
 	q, err := gophercloud.BuildQueryString(opts)
 	if err != nil {
 		return nil, "", err
@@ -125,7 +129,7 @@
 
 	resp, err := perigee.Request("GET", url, perigee.Options{
 		MoreHeaders: h,
-		OkCodes:     []int{200},
+		OkCodes:     []int{200, 304},
 	})
 
 	res.Body = resp.HttpResponse.Body
@@ -146,7 +150,7 @@
 	Metadata           map[string]string
 	ContentDisposition string `h:"Content-Disposition"`
 	ContentEncoding    string `h:"Content-Encoding"`
-	ContentLength      int    `h:"Content-Length"`
+	ContentLength      int64  `h:"Content-Length"`
 	ContentType        string `h:"Content-Type"`
 	CopyFrom           string `h:"X-Copy-From"`
 	DeleteAfter        int    `h:"X-Delete-After"`
diff --git a/openstack/objectstorage/v1/objects/results.go b/openstack/objectstorage/v1/objects/results.go
index 102d94c..b51b840 100644
--- a/openstack/objectstorage/v1/objects/results.go
+++ b/openstack/objectstorage/v1/objects/results.go
@@ -14,11 +14,22 @@
 
 // Object is a structure that holds information related to a storage object.
 type Object struct {
-	Bytes        int    `json:"bytes" mapstructure:"bytes"`
-	ContentType  string `json:"content_type" mapstructure:"content_type"`
-	Hash         string `json:"hash" mapstructure:"hash"`
+	// Bytes is the total number of bytes that comprise the object.
+	Bytes int64 `json:"bytes" mapstructure:"bytes"`
+
+	// ContentType is the content type of the object.
+	ContentType string `json:"content_type" mapstructure:"content_type"`
+
+	// Hash represents the MD5 checksum value of the object's content.
+	Hash string `json:"hash" mapstructure:"hash"`
+
+	// LastModified is the RFC3339Milli time the object was last modified, represented
+	// as a string. For any given object (obj), this value may be parsed to a time.Time:
+	// lastModified, err := time.Parse(gophercloud.RFC3339Milli, obj.LastModified)
 	LastModified string `json:"last_modified" mapstructure:"last_modified"`
-	Name         string `json:"name" mapstructure:"name"`
+
+	// Name is the unique name for the object.
+	Name string `json:"name" mapstructure:"name"`
 }
 
 // ObjectPage is a single page of objects that is returned from a call to the
diff --git a/package.go b/package.go
deleted file mode 100644
index e8c2e82..0000000
--- a/package.go
+++ /dev/null
@@ -1,38 +0,0 @@
-/*
-Package gophercloud provides a multi-vendor interface to OpenStack-compatible
-clouds. The library has a three-level hierarchy: providers, services, and
-resources.
-
-Provider structs represent the service providers that offer and manage a
-collection of services. Examples of providers include: OpenStack, Rackspace,
-HP. These are defined like so:
-
-  opts := gophercloud.AuthOptions{
-    IdentityEndpoint: "https://my-openstack.com:5000/v2.0",
-    Username: "{username}",
-    Password: "{password}",
-    TenantID: "{tenant_id}",
-  }
-
-  provider, err := openstack.AuthenticatedClient(opts)
-
-Service structs are specific to a provider and handle all of the logic and
-operations for a particular OpenStack service. Examples of services include:
-Compute, Object Storage, Block Storage. In order to define one, you need to
-pass in the parent provider, like so:
-
-  opts := gophercloud.EndpointOpts{Region: "RegionOne"}
-
-  client := openstack.NewComputeV2(provider, opts)
-
-Resource structs are the domain models that services make use of in order
-to work with and represent the state of API resources:
-
-  server, err := servers.Get(client, "{serverId}").Extract()
-
-Another convention is to return Result structs for API operations, which allow
-you to access the HTTP headers, response body, and associated errors with the
-network transaction. To get a resource struct, you then call the Extract
-method which is chained to the response.
-*/
-package gophercloud
diff --git a/params.go b/params.go
index 5fe3c2c..68c17eb 100644
--- a/params.go
+++ b/params.go
@@ -9,11 +9,15 @@
 	"time"
 )
 
-// MaybeString takes a string that might be a zero-value, and either returns a
-// pointer to its address or a nil value (i.e. empty pointer). This is useful
-// for converting zero values in options structs when the end-user hasn't
-// defined values. Those zero values need to be nil in order for the JSON
-// serialization to ignore them.
+/*
+MaybeString is an internal function to be used by request methods in individual
+resource packages.
+
+It takes a string that might be a zero value and returns either a pointer to its
+address or nil. This is useful for allowing users to conveniently omit values
+from an options struct by leaving them zeroed, but still pass nil to the JSON
+serializer so they'll be omitted from the request body.
+*/
 func MaybeString(original string) *string {
 	if original != "" {
 		return &original
@@ -21,8 +25,14 @@
 	return nil
 }
 
-// MaybeInt takes an int that might be a zero-value, and either returns a
-// pointer to its address or a nil value (i.e. empty pointer).
+/*
+MaybeInt is an internal function to be used by request methods in individual
+resource packages.
+
+Like MaybeString, it accepts an int that may or may not be a zero value, and
+returns either a pointer to its address or nil. It's intended to hint that the
+JSON serializer should omit its field.
+*/
 func MaybeInt(original int) *int {
 	if original != 0 {
 		return &original
@@ -61,19 +71,26 @@
 }
 
 /*
-BuildQueryString accepts a generic structure and parses it URL struct. It
-converts field names into query names based on "q" tags. So for example, this
-type:
+BuildQueryString is an internal function to be used by request methods in
+individual resource packages.
 
-	struct {
+It accepts a tagged structure and expands it into a URL struct. Field names are
+converted into query parameters based on a "q" tag. For example:
+
+	type struct Something {
 	   Bar string `q:"x_bar"`
 	   Baz int    `q:"lorem_ipsum"`
-	}{
-	   Bar: "XXX",
-	   Baz: "YYY",
 	}
 
-will be converted into ?x_bar=XXX&lorem_ipsum=YYYY
+	instance := Something{
+	   Bar: "AAA",
+	   Baz: "BBB",
+	}
+
+will be converted into "?x_bar=AAA&lorem_ipsum=BBB".
+
+The struct's fields may be strings, integers, or boolean values. Fields left at
+their type's zero value will be omitted from the query.
 */
 func BuildQueryString(opts interface{}) (*url.URL, error) {
 	optsValue := reflect.ValueOf(opts)
@@ -132,9 +149,34 @@
 	return nil, fmt.Errorf("Options type is not a struct.")
 }
 
-// BuildHeaders accepts a generic structure and parses it into a string map. It
-// converts field names into header names based on "h" tags, and field values
-// into header values by a simple one-to-one mapping.
+/*
+BuildHeaders is an internal function to be used by request methods in
+individual resource packages.
+
+It accepts an arbitrary tagged structure and produces a string map that's
+suitable for use as the HTTP headers of an outgoing request. Field names are
+mapped to header names based in "h" tags.
+
+  type struct Something {
+    Bar string `h:"x_bar"`
+    Baz int    `h:"lorem_ipsum"`
+  }
+
+  instance := Something{
+    Bar: "AAA",
+    Baz: "BBB",
+  }
+
+will be converted into:
+
+  map[string]string{
+    "x_bar": "AAA",
+    "lorem_ipsum": "BBB",
+  }
+
+Untagged fields and fields left at their zero values are skipped. Integers,
+booleans and string values are supported.
+*/
 func BuildHeaders(opts interface{}) (map[string]string, error) {
 	optsValue := reflect.ValueOf(opts)
 	if optsValue.Kind() == reflect.Ptr {
diff --git a/rackspace/compute/v2/servers/requests.go b/rackspace/compute/v2/servers/requests.go
index 884b9cb..809183e 100644
--- a/rackspace/compute/v2/servers/requests.go
+++ b/rackspace/compute/v2/servers/requests.go
@@ -43,6 +43,10 @@
 	// ConfigDrive [optional] enables metadata injection through a configuration drive.
 	ConfigDrive bool
 
+	// AdminPass [optional] sets the root user password. If not set, a randomly-generated
+	// password will be created and returned in the response.
+	AdminPass string
+
 	// Rackspace-specific extensions begin here.
 
 	// KeyPair [optional] specifies the name of the SSH KeyPair to be injected into the newly launched
@@ -72,6 +76,7 @@
 		Metadata:         opts.Metadata,
 		Personality:      opts.Personality,
 		ConfigDrive:      opts.ConfigDrive,
+		AdminPass:        opts.AdminPass,
 	}
 
 	drive := diskconfig.CreateOptsExt{
diff --git a/rackspace/identity/v2/roles/delegate.go b/rackspace/identity/v2/roles/delegate.go
new file mode 100644
index 0000000..a6c01e4
--- /dev/null
+++ b/rackspace/identity/v2/roles/delegate.go
@@ -0,0 +1,53 @@
+package roles
+
+import (
+	"github.com/racker/perigee"
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+
+	os "github.com/rackspace/gophercloud/openstack/identity/v2/extensions/admin/roles"
+)
+
+// List is the operation responsible for listing all available global roles
+// that a user can adopt.
+func List(client *gophercloud.ServiceClient) pagination.Pager {
+	return os.List(client)
+}
+
+// AddUserRole is the operation responsible for assigning a particular role to
+// a user. This is confined to the scope of the user's tenant - so the tenant
+// ID is a required argument.
+func AddUserRole(client *gophercloud.ServiceClient, userID, roleID string) UserRoleResult {
+	var result UserRoleResult
+
+	_, result.Err = perigee.Request("PUT", userRoleURL(client, userID, roleID), perigee.Options{
+		MoreHeaders: client.AuthenticatedHeaders(),
+		OkCodes:     []int{200, 201},
+	})
+
+	return result
+}
+
+// DeleteUserRole is the operation responsible for deleting a particular role
+// from a user. This is confined to the scope of the user's tenant - so the
+// tenant ID is a required argument.
+func DeleteUserRole(client *gophercloud.ServiceClient, userID, roleID string) UserRoleResult {
+	var result UserRoleResult
+
+	_, result.Err = perigee.Request("DELETE", userRoleURL(client, userID, roleID), perigee.Options{
+		MoreHeaders: client.AuthenticatedHeaders(),
+		OkCodes:     []int{204},
+	})
+
+	return result
+}
+
+// UserRoleResult represents the result of either an AddUserRole or
+// a DeleteUserRole operation.
+type UserRoleResult struct {
+	gophercloud.ErrResult
+}
+
+func userRoleURL(c *gophercloud.ServiceClient, userID, roleID string) string {
+	return c.ServiceURL(os.UserPath, userID, os.RolePath, os.ExtPath, roleID)
+}
diff --git a/rackspace/identity/v2/roles/delegate_test.go b/rackspace/identity/v2/roles/delegate_test.go
new file mode 100644
index 0000000..fcee97d
--- /dev/null
+++ b/rackspace/identity/v2/roles/delegate_test.go
@@ -0,0 +1,66 @@
+package roles
+
+import (
+	"testing"
+
+	os "github.com/rackspace/gophercloud/openstack/identity/v2/extensions/admin/roles"
+	"github.com/rackspace/gophercloud/pagination"
+	th "github.com/rackspace/gophercloud/testhelper"
+	"github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestRole(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	MockListRoleResponse(t)
+
+	count := 0
+
+	err := List(client.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := os.ExtractRoles(page)
+		if err != nil {
+			t.Errorf("Failed to extract users: %v", err)
+			return false, err
+		}
+
+		expected := []os.Role{
+			os.Role{
+				ID:          "123",
+				Name:        "compute:admin",
+				Description: "Nova Administrator",
+				ServiceID:   "cke5372ebabeeabb70a0e702a4626977x4406e5",
+			},
+		}
+
+		th.CheckDeepEquals(t, expected, actual)
+
+		return true, nil
+	})
+
+	th.AssertNoErr(t, err)
+	th.AssertEquals(t, 1, count)
+}
+
+func TestAddUserRole(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	MockAddUserRoleResponse(t)
+
+	err := AddUserRole(client.ServiceClient(), "{user_id}", "{role_id}").ExtractErr()
+
+	th.AssertNoErr(t, err)
+}
+
+func TestDeleteUserRole(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	MockDeleteUserRoleResponse(t)
+
+	err := DeleteUserRole(client.ServiceClient(), "{user_id}", "{role_id}").ExtractErr()
+
+	th.AssertNoErr(t, err)
+}
diff --git a/rackspace/identity/v2/roles/fixtures.go b/rackspace/identity/v2/roles/fixtures.go
new file mode 100644
index 0000000..5f22d0f
--- /dev/null
+++ b/rackspace/identity/v2/roles/fixtures.go
@@ -0,0 +1,49 @@
+package roles
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	th "github.com/rackspace/gophercloud/testhelper"
+	fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func MockListRoleResponse(t *testing.T) {
+	th.Mux.HandleFunc("/OS-KSADM/roles", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusOK)
+
+		fmt.Fprintf(w, `
+{
+    "roles": [
+        {
+            "id": "123",
+            "name": "compute:admin",
+            "description": "Nova Administrator",
+            "serviceId": "cke5372ebabeeabb70a0e702a4626977x4406e5"
+        }
+    ]
+}
+  `)
+	})
+}
+
+func MockAddUserRoleResponse(t *testing.T) {
+	th.Mux.HandleFunc("/users/{user_id}/roles/OS-KSADM/{role_id}", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "PUT")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+		w.WriteHeader(http.StatusCreated)
+	})
+}
+
+func MockDeleteUserRoleResponse(t *testing.T) {
+	th.Mux.HandleFunc("/users/{user_id}/roles/OS-KSADM/{role_id}", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "DELETE")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+		w.WriteHeader(http.StatusNoContent)
+	})
+}
diff --git a/rackspace/identity/v2/users/delegate.go b/rackspace/identity/v2/users/delegate.go
new file mode 100644
index 0000000..ae2acde
--- /dev/null
+++ b/rackspace/identity/v2/users/delegate.go
@@ -0,0 +1,145 @@
+package users
+
+import (
+	"errors"
+
+	"github.com/racker/perigee"
+	"github.com/rackspace/gophercloud"
+	os "github.com/rackspace/gophercloud/openstack/identity/v2/users"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// List returns a pager that allows traversal over a collection of users.
+func List(client *gophercloud.ServiceClient) pagination.Pager {
+	return os.List(client)
+}
+
+// CommonOpts are the options which are shared between CreateOpts and
+// UpdateOpts
+type CommonOpts struct {
+	// Required. The username to assign to the user. When provided, the username
+	// must:
+	// - start with an alphabetical (A-Za-z) character
+	// - have a minimum length of 1 character
+	//
+	// The username may contain upper and lowercase characters, as well as any of
+	// the following special character: . - @ _
+	Username string
+
+	// Required. Email address for the user account.
+	Email string
+
+	// Required. Indicates whether the user can authenticate after the user
+	// account is created. If no value is specified, the default value is true.
+	Enabled os.EnabledState
+
+	// Optional. The password to assign to the user. If provided, the password
+	// must:
+	// - start with an alphabetical (A-Za-z) character
+	// - have a minimum length of 8 characters
+	// - contain at least one uppercase character, one lowercase character, and
+	//   one numeric character.
+	//
+	// The password may contain any of the following special characters: . - @ _
+	Password string
+}
+
+// CreateOpts represents the options needed when creating new users.
+type CreateOpts CommonOpts
+
+// ToUserCreateMap assembles a request body based on the contents of a CreateOpts.
+func (opts CreateOpts) ToUserCreateMap() (map[string]interface{}, error) {
+	m := make(map[string]interface{})
+
+	if opts.Username == "" {
+		return m, errors.New("Username is a required field")
+	}
+	if opts.Enabled == nil {
+		return m, errors.New("Enabled is a required field")
+	}
+	if opts.Email == "" {
+		return m, errors.New("Email is a required field")
+	}
+
+	if opts.Username != "" {
+		m["username"] = opts.Username
+	}
+	if opts.Email != "" {
+		m["email"] = opts.Email
+	}
+	if opts.Enabled != nil {
+		m["enabled"] = opts.Enabled
+	}
+	if opts.Password != "" {
+		m["OS-KSADM:password"] = opts.Password
+	}
+
+	return map[string]interface{}{"user": m}, nil
+}
+
+// Create is the operation responsible for creating new users.
+func Create(client *gophercloud.ServiceClient, opts os.CreateOptsBuilder) CreateResult {
+	return CreateResult{os.Create(client, opts)}
+}
+
+// Get requests details on a single user, either by ID.
+func Get(client *gophercloud.ServiceClient, id string) GetResult {
+	return GetResult{os.Get(client, id)}
+}
+
+// UpdateOptsBuilder allows extensions to add additional attributes to the Update request.
+type UpdateOptsBuilder interface {
+	ToUserUpdateMap() map[string]interface{}
+}
+
+// UpdateOpts specifies the base attributes that may be updated on an existing server.
+type UpdateOpts CommonOpts
+
+// ToUserUpdateMap formats an UpdateOpts structure into a request body.
+func (opts UpdateOpts) ToUserUpdateMap() map[string]interface{} {
+	m := make(map[string]interface{})
+
+	if opts.Username != "" {
+		m["username"] = opts.Username
+	}
+	if opts.Enabled != nil {
+		m["enabled"] = &opts.Enabled
+	}
+	if opts.Email != "" {
+		m["email"] = opts.Email
+	}
+
+	return map[string]interface{}{"user": m}
+}
+
+// Update is the operation responsible for updating exist users by their UUID.
+func Update(client *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) UpdateResult {
+	var result UpdateResult
+
+	_, result.Err = perigee.Request("POST", os.ResourceURL(client, id), perigee.Options{
+		Results:     &result.Body,
+		ReqBody:     opts.ToUserUpdateMap(),
+		MoreHeaders: client.AuthenticatedHeaders(),
+		OkCodes:     []int{200},
+	})
+
+	return result
+}
+
+// Delete is the operation responsible for permanently deleting an API user.
+func Delete(client *gophercloud.ServiceClient, id string) os.DeleteResult {
+	return os.Delete(client, id)
+}
+
+// ResetAPIKey resets the User's API key.
+func ResetAPIKey(client *gophercloud.ServiceClient, id string) ResetAPIKeyResult {
+	var result ResetAPIKeyResult
+
+	_, result.Err = perigee.Request("POST", resetAPIKeyURL(client, id), perigee.Options{
+		Results:     &result.Body,
+		MoreHeaders: client.AuthenticatedHeaders(),
+		OkCodes:     []int{200},
+	})
+
+	return result
+}
diff --git a/rackspace/identity/v2/users/delegate_test.go b/rackspace/identity/v2/users/delegate_test.go
new file mode 100644
index 0000000..62faf0c
--- /dev/null
+++ b/rackspace/identity/v2/users/delegate_test.go
@@ -0,0 +1,111 @@
+package users
+
+import (
+	"testing"
+
+	os "github.com/rackspace/gophercloud/openstack/identity/v2/users"
+	"github.com/rackspace/gophercloud/pagination"
+	th "github.com/rackspace/gophercloud/testhelper"
+	"github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestList(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	mockListResponse(t)
+
+	count := 0
+
+	err := List(client.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		users, err := os.ExtractUsers(page)
+
+		th.AssertNoErr(t, err)
+		th.AssertEquals(t, "u1000", users[0].ID)
+		th.AssertEquals(t, "u1001", users[1].ID)
+
+		return true, nil
+	})
+
+	th.AssertNoErr(t, err)
+	th.AssertEquals(t, 1, count)
+}
+
+func TestCreateUser(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	mockCreateUser(t)
+
+	opts := CreateOpts{
+		Username: "new_user",
+		Enabled:  os.Disabled,
+		Email:    "new_user@foo.com",
+		Password: "foo",
+	}
+
+	user, err := Create(client.ServiceClient(), opts).Extract()
+
+	th.AssertNoErr(t, err)
+
+	th.AssertEquals(t, "123456", user.ID)
+	th.AssertEquals(t, "5830280", user.DomainID)
+	th.AssertEquals(t, "DFW", user.DefaultRegion)
+}
+
+func TestGetUser(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	mockGetUser(t)
+
+	user, err := Get(client.ServiceClient(), "new_user").Extract()
+	th.AssertNoErr(t, err)
+
+	th.AssertEquals(t, true, user.Enabled)
+	th.AssertEquals(t, true, user.MultiFactorEnabled)
+}
+
+func TestUpdateUser(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	mockUpdateUser(t)
+
+	id := "c39e3de9be2d4c779f1dfd6abacc176d"
+
+	opts := UpdateOpts{
+		Enabled: os.Enabled,
+		Email:   "new_email@foo.com",
+	}
+
+	user, err := Update(client.ServiceClient(), id, opts).Extract()
+
+	th.AssertNoErr(t, err)
+
+	th.AssertEquals(t, true, user.Enabled)
+	th.AssertEquals(t, "new_email@foo.com", user.Email)
+}
+
+func TestDeleteServer(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	mockDeleteUser(t)
+
+	res := Delete(client.ServiceClient(), "c39e3de9be2d4c779f1dfd6abacc176d")
+	th.AssertNoErr(t, res.Err)
+}
+
+func TestResetAPIKey(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	mockResetAPIKey(t)
+
+	apiKey, err := ResetAPIKey(client.ServiceClient(), "99").Extract()
+	th.AssertNoErr(t, err)
+	th.AssertEquals(t, "joesmith", apiKey.Username)
+	th.AssertEquals(t, "mooH1eiLahd5ahYood7r", apiKey.APIKey)
+}
diff --git a/rackspace/identity/v2/users/fixtures.go b/rackspace/identity/v2/users/fixtures.go
new file mode 100644
index 0000000..973f39e
--- /dev/null
+++ b/rackspace/identity/v2/users/fixtures.go
@@ -0,0 +1,154 @@
+package users
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	th "github.com/rackspace/gophercloud/testhelper"
+	fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func mockListResponse(t *testing.T) {
+	th.Mux.HandleFunc("/users", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusOK)
+
+		fmt.Fprintf(w, `
+{
+		"users":[
+				{
+						"id": "u1000",
+						"username": "jqsmith",
+						"email": "john.smith@example.org",
+						"enabled": true
+				},
+				{
+						"id": "u1001",
+						"username": "jqsmith",
+						"email": "jane.smith@example.org",
+						"enabled": true
+				}
+		]
+}
+	`)
+	})
+}
+
+func mockCreateUser(t *testing.T) {
+	th.Mux.HandleFunc("/users", 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, `
+{
+    "user": {
+        "username": "new_user",
+        "enabled": false,
+        "email": "new_user@foo.com",
+				"OS-KSADM:password": "foo"
+    }
+}
+  `)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusOK)
+
+		fmt.Fprintf(w, `
+{
+  "user": {
+    "RAX-AUTH:defaultRegion": "DFW",
+    "RAX-AUTH:domainId": "5830280",
+    "id": "123456",
+    "username": "new_user",
+    "email": "new_user@foo.com",
+    "enabled": false
+  }
+}
+`)
+	})
+}
+
+func mockGetUser(t *testing.T) {
+	th.Mux.HandleFunc("/users/new_user", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusOK)
+
+		fmt.Fprintf(w, `
+{
+	"user": {
+		"RAX-AUTH:defaultRegion": "DFW",
+		"RAX-AUTH:domainId": "5830280",
+		"RAX-AUTH:multiFactorEnabled": "true",
+		"id": "c39e3de9be2d4c779f1dfd6abacc176d",
+		"username": "jqsmith",
+		"email": "john.smith@example.org",
+		"enabled": true
+	}
+}
+`)
+	})
+}
+
+func mockUpdateUser(t *testing.T) {
+	th.Mux.HandleFunc("/users/c39e3de9be2d4c779f1dfd6abacc176d", 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, `
+{
+	"user": {
+		"email": "new_email@foo.com",
+		"enabled": true
+	}
+}
+`)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusOK)
+
+		fmt.Fprintf(w, `
+{
+	"user": {
+		"RAX-AUTH:defaultRegion": "DFW",
+		"RAX-AUTH:domainId": "5830280",
+		"RAX-AUTH:multiFactorEnabled": "true",
+		"id": "123456",
+		"username": "jqsmith",
+		"email": "new_email@foo.com",
+		"enabled": true
+	}
+}
+`)
+	})
+}
+
+func mockDeleteUser(t *testing.T) {
+	th.Mux.HandleFunc("/users/c39e3de9be2d4c779f1dfd6abacc176d", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "DELETE")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+		w.WriteHeader(http.StatusNoContent)
+	})
+}
+
+func mockResetAPIKey(t *testing.T) {
+	th.Mux.HandleFunc("/users/99/OS-KSADM/credentials/RAX-KSKEY:apiKeyCredentials/RAX-AUTH/reset", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "POST")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusOK)
+		fmt.Fprintf(w, `
+{
+    "RAX-KSKEY:apiKeyCredentials": {
+        "username": "joesmith",
+        "apiKey": "mooH1eiLahd5ahYood7r"
+    }
+}`)
+	})
+}
diff --git a/rackspace/identity/v2/users/results.go b/rackspace/identity/v2/users/results.go
new file mode 100644
index 0000000..6936ecb
--- /dev/null
+++ b/rackspace/identity/v2/users/results.go
@@ -0,0 +1,129 @@
+package users
+
+import (
+	"strconv"
+
+	"github.com/rackspace/gophercloud"
+	os "github.com/rackspace/gophercloud/openstack/identity/v2/users"
+
+	"github.com/mitchellh/mapstructure"
+)
+
+// User represents a user resource that exists on the API.
+type User struct {
+	// The UUID for this user.
+	ID string
+
+	// The human name for this user.
+	Name string
+
+	// The username for this user.
+	Username string
+
+	// Indicates whether the user is enabled (true) or disabled (false).
+	Enabled bool
+
+	// The email address for this user.
+	Email string
+
+	// The ID of the tenant to which this user belongs.
+	TenantID string `mapstructure:"tenant_id"`
+
+	// Specifies the default region for the user account. This value is inherited
+	// from the user administrator when the account is created.
+	DefaultRegion string `mapstructure:"RAX-AUTH:defaultRegion"`
+
+	// Identifies the domain that contains the user account. This value is
+	// inherited from the user administrator when the account is created.
+	DomainID string `mapstructure:"RAX-AUTH:domainId"`
+
+	// The password value that the user needs for authentication. If the Add user
+	// request included a password value, this attribute is not included in the
+	// response.
+	Password string `mapstructure:"OS-KSADM:password"`
+
+	// Indicates whether the user has enabled multi-factor authentication.
+	MultiFactorEnabled bool `mapstructure:"RAX-AUTH:multiFactorEnabled"`
+}
+
+// CreateResult represents the result of a Create operation
+type CreateResult struct {
+	os.CreateResult
+}
+
+// GetResult represents the result of a Get operation
+type GetResult struct {
+	os.GetResult
+}
+
+// UpdateResult represents the result of an Update operation
+type UpdateResult struct {
+	os.UpdateResult
+}
+
+func commonExtract(resp interface{}, err error) (*User, error) {
+	if err != nil {
+		return nil, err
+	}
+
+	var respStruct struct {
+		User *User `json:"user"`
+	}
+
+	// Since the API returns a string instead of a bool, we need to hack the JSON
+	json := resp.(map[string]interface{})
+	user := json["user"].(map[string]interface{})
+	if s, ok := user["RAX-AUTH:multiFactorEnabled"].(string); ok && s != "" {
+		if b, err := strconv.ParseBool(s); err == nil {
+			user["RAX-AUTH:multiFactorEnabled"] = b
+		}
+	}
+
+	err = mapstructure.Decode(json, &respStruct)
+
+	return respStruct.User, err
+}
+
+// Extract will get the Snapshot object out of the GetResult object.
+func (r GetResult) Extract() (*User, error) {
+	return commonExtract(r.Body, r.Err)
+}
+
+// Extract will get the Snapshot object out of the CreateResult object.
+func (r CreateResult) Extract() (*User, error) {
+	return commonExtract(r.Body, r.Err)
+}
+
+// Extract will get the Snapshot object out of the UpdateResult object.
+func (r UpdateResult) Extract() (*User, error) {
+	return commonExtract(r.Body, r.Err)
+}
+
+// ResetAPIKeyResult represents the server response to the ResetAPIKey method.
+type ResetAPIKeyResult struct {
+	gophercloud.Result
+}
+
+// ResetAPIKeyValue represents an API Key that has been reset.
+type ResetAPIKeyValue struct {
+	// The Username for this API Key reset.
+	Username string `mapstructure:"username"`
+
+	// The new API Key for this user.
+	APIKey string `mapstructure:"apiKey"`
+}
+
+// Extract will get the Error or ResetAPIKeyValue object out of the ResetAPIKeyResult object.
+func (r ResetAPIKeyResult) Extract() (*ResetAPIKeyValue, error) {
+	if r.Err != nil {
+		return nil, r.Err
+	}
+
+	var response struct {
+		ResetAPIKeyValue ResetAPIKeyValue `mapstructure:"RAX-KSKEY:apiKeyCredentials"`
+	}
+
+	err := mapstructure.Decode(r.Body, &response)
+
+	return &response.ResetAPIKeyValue, err
+}
diff --git a/rackspace/identity/v2/users/urls.go b/rackspace/identity/v2/users/urls.go
new file mode 100644
index 0000000..bc1aaef
--- /dev/null
+++ b/rackspace/identity/v2/users/urls.go
@@ -0,0 +1,7 @@
+package users
+
+import "github.com/rackspace/gophercloud"
+
+func resetAPIKeyURL(client *gophercloud.ServiceClient, id string) string {
+	return client.ServiceURL("users", id, "OS-KSADM", "credentials", "RAX-KSKEY:apiKeyCredentials", "RAX-AUTH", "reset")
+}
diff --git a/results.go b/results.go
index f480bc7..3fd5029 100644
--- a/results.go
+++ b/results.go
@@ -5,21 +5,37 @@
 	"net/http"
 )
 
-// Result acts as a base struct that other results can embed.
+/*
+Result is an internal type to be used by individual resource packages, but its
+methods will be available on a wide variety of user-facing embedding types.
+
+It acts as a base struct that other Result types, returned from request
+functions, can embed for convenience. All Results capture basic information
+from the HTTP transaction that was performed, including the response body,
+HTTP headers, and any errors that happened.
+
+Generally, each Result type will have an Extract method that can be used to
+further interpret the result's payload in a specific context. Extensions or
+providers can then provide additional extraction functions to pull out
+provider- or extension-specific information as well.
+*/
 type Result struct {
-	// Body is the payload of the HTTP response from the server. In most cases, this will be the
-	// deserialized JSON structure.
+	// Body is the payload of the HTTP response from the server. In most cases,
+	// this will be the deserialized JSON structure.
 	Body interface{}
 
 	// Header contains the HTTP header structure from the original response.
 	Header http.Header
 
-	// Err is an error that occurred during the operation. It's deferred until extraction to make
-	// it easier to chain operations.
+	// Err is an error that occurred during the operation. It's deferred until
+	// extraction to make it easier to chain the Extract call.
 	Err error
 }
 
-// PrettyPrintJSON creates a string containing the full response body as pretty-printed JSON.
+// PrettyPrintJSON creates a string containing the full response body as
+// pretty-printed JSON. It's useful for capturing test fixtures and for
+// debugging extraction bugs. If you include its output in an issue related to
+// a buggy extraction function, we will all love you forever.
 func (r Result) PrettyPrintJSON() string {
 	pretty, err := json.MarshalIndent(r.Body, "", "  ")
 	if err != nil {
@@ -28,44 +44,67 @@
 	return string(pretty)
 }
 
-// ErrResult represents results that only contain a potential error and
-// nothing else. Usually if the operation executed successfully, the Err field
-// will be nil; otherwise it will be stocked with a relevant error.
+// ErrResult is an internal type to be used by individual resource packages, but
+// its methods will be available on a wide variety of user-facing embedding
+// types.
+//
+// It represents results that only contain a potential error and
+// nothing else. Usually, if the operation executed successfully, the Err field
+// will be nil; otherwise it will be stocked with a relevant error. Use the
+// ExtractErr method
+// to cleanly pull it out.
 type ErrResult struct {
 	Result
 }
 
-// ExtractErr is a function that extracts error information from a result.
+// ExtractErr is a function that extracts error information, or nil, from a result.
 func (r ErrResult) ExtractErr() error {
 	return r.Err
 }
 
-// HeaderResult represents a result that only contains an `error` (possibly nil)
-// and an http.Header. This is used, for example, by the `objectstorage` packages
-// in `openstack`, because most of the operations don't return response bodies.
+/*
+HeaderResult is an internal type to be used by individual resource packages, but
+its methods will be available on a wide variety of user-facing embedding types.
+
+It represents a result that only contains an error (possibly nil) and an
+http.Header. This is used, for example, by the objectstorage packages in
+openstack, because most of the operations don't return response bodies, but do
+have relevant information in headers.
+*/
 type HeaderResult struct {
 	Result
 }
 
 // ExtractHeader will return the http.Header and error from the HeaderResult.
-// Usage: header, err := objects.Create(client, "my_container", objects.CreateOpts{}).ExtractHeader()
+//
+//   header, err := objects.Create(client, "my_container", objects.CreateOpts{}).ExtractHeader()
 func (hr HeaderResult) ExtractHeader() (http.Header, error) {
 	return hr.Header, hr.Err
 }
 
-// RFC3339Milli describes a time format used by API responses.
+// RFC3339Milli describes a common time format used by some API responses.
 const RFC3339Milli = "2006-01-02T15:04:05.999999Z"
 
-// Link represents a structure that enables paginated collections how to
-// traverse backward or forward. The "Rel" field is usually either "next".
+/*
+Link is an internal type to be used in packages of collection resources that are
+paginated in a certain way.
+
+It's a response substructure common to many paginated collection results that is
+used to point to related pages. Usually, the one we care about is the one with
+Rel field set to "next".
+*/
 type Link struct {
 	Href string `mapstructure:"href"`
 	Rel  string `mapstructure:"rel"`
 }
 
-// ExtractNextURL attempts to extract the next URL from a JSON structure. It
-// follows the common convention of nesting back and next URLs in a "links"
-// JSON array.
+/*
+ExtractNextURL is an internal function useful for packages of collection
+resources that are paginated in a certain way.
+
+It attempts attempts to extract the "next" URL from slice of Link structs, or
+"" if no such URL is present.
+*/
 func ExtractNextURL(links []Link) (string, error) {
 	var url string
 
diff --git a/util.go b/util.go
index 101fd39..fbd9fe9 100644
--- a/util.go
+++ b/util.go
@@ -7,7 +7,9 @@
 )
 
 // WaitFor polls a predicate function, once per second, up to a timeout limit.
-// It usually does this to wait for the resource to transition to a certain state.
+// It usually does this to wait for a resource to transition to a certain state.
+// Resource packages will wrap this in a more convenient function that's
+// specific to a certain resource, but it can also be useful on its own.
 func WaitFor(timeout int, predicate func() (bool, error)) error {
 	start := time.Now().Second()
 	for {
@@ -30,7 +32,10 @@
 	}
 }
 
-// NormalizeURL ensures that each endpoint URL has a closing `/`, as expected by ServiceClient.
+// NormalizeURL is an internal function to be used by provider clients.
+//
+// It ensures that each endpoint URL has a closing `/`, as expected by
+// ServiceClient's methods.
 func NormalizeURL(url string) string {
 	if !strings.HasSuffix(url, "/") {
 		return url + "/"