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 + "/"