Merge pull request #532 from rickard-von-essen/clearify_docs_about_tags

Updated docs to provide information about tags needed to run tests.
diff --git a/.travis.yml b/.travis.yml
index 325b90a..42c095d 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,16 +1,20 @@
 language: go
+sudo: false
 install:
+  - go get golang.org/x/crypto/ssh
   - go get -v -tags 'fixtures acceptance' ./...
 go:
-  - 1.2
-  - 1.3
   - 1.4
   - 1.5
-script: script/cibuild
-after_success:
-  - go get golang.org/x/tools/cmd/cover
+  - tip
+env:
+  - COVERALLS_TOKEN=2k7PTU3xa474Hymwgdj6XjqenNfGTNkO8
+before_install:
   - go get github.com/axw/gocov/gocov
   - go get github.com/mattn/goveralls
-  - export PATH=$PATH:$HOME/gopath/bin/
-  - goveralls 2k7PTU3xa474Hymwgdj6XjqenNfGTNkO8
-sudo: false
+  - go get github.com/pierrre/gotestcover
+  - if ! go get github.com/golang/tools/cmd/cover; then go get golang.org/x/tools/cmd/cover; fi
+script:
+  - $HOME/gopath/bin/gotestcover -v -tags=fixtures -coverprofile=cover.out ./...
+after_success:
+  - $HOME/gopath/bin/goveralls -service=travis-ci -coverprofile=cover.out
diff --git a/README.md b/README.md
index 0a0da59..4b0042b 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,5 @@
 # Gophercloud: an OpenStack SDK for Go
-[![Build Status](https://travis-ci.org/rackspace/gophercloud.svg?branch=master)](https://travis-ci.org/rackspace/gophercloud)
+[![Build Status](https://travis-ci.org/rackspace/gophercloud.svg?branch=master)](https://travis-ci.org/rackspace/gophercloud) [![Coverage Status](https://coveralls.io/repos/rackspace/gophercloud/badge.png)](https://coveralls.io/r/rackspace/gophercloud)
 
 Gophercloud is a flexible SDK that allows you to consume and work with OpenStack
 clouds in a simple and idiomatic way using golang. Many services are supported,
diff --git a/acceptance/openstack/blockstorage/v1/pkg.go b/acceptance/openstack/blockstorage/v1/pkg.go
new file mode 100644
index 0000000..16d2567
--- /dev/null
+++ b/acceptance/openstack/blockstorage/v1/pkg.go
@@ -0,0 +1,3 @@
+// The v1 package contains acceptance tests for the Openstack Cinder V1 service.
+
+package v1
diff --git a/acceptance/rackspace/identity/v2/tokens_test.go b/acceptance/rackspace/identity/v2/tokens_test.go
index 95ee7e6..b241cd9 100644
--- a/acceptance/rackspace/identity/v2/tokens_test.go
+++ b/acceptance/rackspace/identity/v2/tokens_test.go
@@ -6,43 +6,10 @@
 	"os"
 	"testing"
 
-	"github.com/rackspace/gophercloud"
-	"github.com/rackspace/gophercloud/acceptance/tools"
-	"github.com/rackspace/gophercloud/rackspace"
 	"github.com/rackspace/gophercloud/rackspace/identity/v2/tokens"
 	th "github.com/rackspace/gophercloud/testhelper"
 )
 
-func rackspaceAuthOptions(t *testing.T) gophercloud.AuthOptions {
-	// Obtain credentials from the environment.
-	options, err := rackspace.AuthOptionsFromEnv()
-	th.AssertNoErr(t, err)
-	options = tools.OnlyRS(options)
-
-	if options.Username == "" {
-		t.Fatal("Please provide a Rackspace username as RS_USERNAME.")
-	}
-	if options.APIKey == "" {
-		t.Fatal("Please provide a Rackspace API key as RS_API_KEY.")
-	}
-
-	return options
-}
-
-func createClient(t *testing.T, auth bool) *gophercloud.ServiceClient {
-	ao := rackspaceAuthOptions(t)
-
-	provider, err := rackspace.NewClient(ao.IdentityEndpoint)
-	th.AssertNoErr(t, err)
-
-	if auth {
-		err = rackspace.Authenticate(provider, ao)
-		th.AssertNoErr(t, err)
-	}
-
-	return rackspace.NewIdentityV2(provider)
-}
-
 func TestTokenAuth(t *testing.T) {
 	authedClient := createClient(t, true)
 	token := authedClient.TokenID
@@ -54,7 +21,7 @@
 
 	authOpts := tokens.AuthOptions{}
 	authOpts.TenantID = tenantID
-	authOpts.Token = token
+	authOpts.TokenID = token
 
 	_, err := tokens.Create(authedClient, authOpts).ExtractToken()
 	th.AssertNoErr(t, err)
diff --git a/openstack/compute/v2/servers/fixtures.go b/openstack/compute/v2/servers/fixtures.go
index 151fea2..85cea70 100644
--- a/openstack/compute/v2/servers/fixtures.go
+++ b/openstack/compute/v2/servers/fixtures.go
@@ -235,6 +235,12 @@
 }
 `
 
+const ServerPasswordBody = `
+{
+    "password": "xlozO3wLCBRWAa2yDjCCVx8vwNPypxnypmRYDa/zErlQ+EzPe1S/Gz6nfmC52mOlOSCRuUOmG7kqqgejPof6M7bOezS387zjq4LSvvwp28zUknzy4YzfFGhnHAdai3TxUJ26pfQCYrq8UTzmKF2Bq8ioSEtVVzM0A96pDh8W2i7BOz6MdoiVyiev/I1K2LsuipfxSJR7Wdke4zNXJjHHP2RfYsVbZ/k9ANu+Nz4iIH8/7Cacud/pphH7EjrY6a4RZNrjQskrhKYed0YERpotyjYk1eDtRe72GrSiXteqCM4biaQ5w3ruS+AcX//PXk3uJ5kC7d67fPXaVz4WaQRYMg=="
+}
+`
+
 var (
 	// ServerHerp is a Server struct that should correspond to the first result in ServerListBody.
 	ServerHerp = Server{
@@ -399,8 +405,8 @@
 	})
 }
 
-// HandleAdminPasswordChangeSuccessfully sets up the test server to respond to a server password
-// change request.
+// HandleServerForceDeletionSuccessfully sets up the test server to respond to a server force deletion
+// request.
 func HandleServerForceDeletionSuccessfully(t *testing.T) {
 	th.Mux.HandleFunc("/servers/asdfasdfasdf/action", func(w http.ResponseWriter, r *http.Request) {
 		th.TestMethod(t, r, "POST")
@@ -674,3 +680,13 @@
 	})
 }
 
+// HandlePasswordGetSuccessfully sets up the test server to respond to a password Get request.
+func HandlePasswordGetSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/servers/1234asdf/os-server-password", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+		th.TestHeader(t, r, "Accept", "application/json")
+
+		fmt.Fprintf(w, ServerPasswordBody)
+	})
+}
diff --git a/openstack/compute/v2/servers/requests.go b/openstack/compute/v2/servers/requests.go
index 8e60daa..e649053 100644
--- a/openstack/compute/v2/servers/requests.go
+++ b/openstack/compute/v2/servers/requests.go
@@ -861,3 +861,12 @@
 		return "", fmt.Errorf("Found %d servers matching %s", serverCount, name)
 	}
 }
+
+// GetPassword makes a request against the nova API to get the encrypted administrative password.
+func GetPassword(client *gophercloud.ServiceClient, serverId string) GetPasswordResult {
+	var res GetPasswordResult
+	_, res.Err = client.Request("GET", passwordURL(client, serverId), gophercloud.RequestOpts{
+		JSONResponse: &res.Body,
+	})
+	return res
+}
diff --git a/openstack/compute/v2/servers/requests_test.go b/openstack/compute/v2/servers/requests_test.go
index e042074..6e23c52 100644
--- a/openstack/compute/v2/servers/requests_test.go
+++ b/openstack/compute/v2/servers/requests_test.go
@@ -124,6 +124,15 @@
 	th.AssertNoErr(t, res.Err)
 }
 
+func TestGetPassword(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandlePasswordGetSuccessfully(t)
+
+	res := GetPassword(client.ServiceClient(), "1234asdf")
+	th.AssertNoErr(t, res.Err)
+}
+
 func TestRebootServer(t *testing.T) {
 	th.SetupHTTP()
 	defer th.TeardownHTTP()
diff --git a/openstack/compute/v2/servers/results.go b/openstack/compute/v2/servers/results.go
index f278709..406f689 100644
--- a/openstack/compute/v2/servers/results.go
+++ b/openstack/compute/v2/servers/results.go
@@ -1,10 +1,12 @@
 package servers
 
 import (
-	"reflect"
+	"crypto/rsa"
+	"encoding/base64"
 	"fmt"
-	"path"
 	"net/url"
+	"path"
+	"reflect"
 
 	"github.com/mitchellh/mapstructure"
 	"github.com/rackspace/gophercloud"
@@ -82,6 +84,47 @@
 	gophercloud.Result
 }
 
+// GetPasswordResult represent the result of a get os-server-password operation.
+type GetPasswordResult struct {
+	gophercloud.Result
+}
+
+// ExtractPassword gets the encrypted password.
+// If privateKey != nil the password is decrypted with the private key.
+// If privateKey == nil the encrypted password is returned and can be decrypted with:
+//   echo '<pwd>' | base64 -D | openssl rsautl -decrypt -inkey <private_key>
+func (r GetPasswordResult) ExtractPassword(privateKey *rsa.PrivateKey) (string, error) {
+
+	if r.Err != nil {
+		return "", r.Err
+	}
+
+	var response struct {
+		Password string `mapstructure:"password"`
+	}
+
+	err := mapstructure.Decode(r.Body, &response)
+	if err == nil && privateKey != nil && response.Password != "" {
+		return decryptPassword(response.Password, privateKey)
+	}
+	return response.Password, err
+}
+
+func decryptPassword(encryptedPassword string, privateKey *rsa.PrivateKey) (string, error) {
+	b64EncryptedPassword := make([]byte, base64.StdEncoding.DecodedLen(len(encryptedPassword)))
+
+	n, err := base64.StdEncoding.Decode(b64EncryptedPassword, []byte(encryptedPassword))
+	if err != nil {
+		return "", fmt.Errorf("Failed to base64 decode encrypted password: %s", err)
+	}
+	password, err := rsa.DecryptPKCS1v15(nil, privateKey, b64EncryptedPassword[0:n])
+	if err != nil {
+		return "", fmt.Errorf("Failed to decrypt password: %s", err)
+	}
+
+	return string(password), nil
+}
+
 // ExtractImageID gets the ID of the newly created server image from the header
 func (res CreateImageResult) ExtractImageID() (string, error) {
 	if res.Err != nil {
diff --git a/openstack/compute/v2/servers/results_test.go b/openstack/compute/v2/servers/results_test.go
new file mode 100644
index 0000000..2dba484
--- /dev/null
+++ b/openstack/compute/v2/servers/results_test.go
@@ -0,0 +1,99 @@
+// +build fixtures
+
+package servers
+
+import (
+	"crypto/rsa"
+	"encoding/json"
+	"fmt"
+	"testing"
+
+	"github.com/rackspace/gophercloud"
+	th "github.com/rackspace/gophercloud/testhelper"
+	"golang.org/x/crypto/ssh"
+)
+
+// Fail - No password in JSON.
+func TestExtractPassword_no_pwd_data(t *testing.T) {
+
+	var dejson interface{}
+	err := json.Unmarshal([]byte(`{ "Crappy data": ".-.-." }`), &dejson)
+	if err != nil {
+		t.Fatalf("%s", err)
+	}
+	resp := GetPasswordResult{gophercloud.Result{Body: dejson}}
+
+	pwd, err := resp.ExtractPassword(nil)
+	th.AssertEquals(t, pwd, "")
+}
+
+// Ok - return encrypted password when no private key is given.
+func TestExtractPassword_encrypted_pwd(t *testing.T) {
+
+	var dejson interface{}
+	sejson := []byte(`{"password":"PP8EnwPO9DhEc8+O/6CKAkPF379mKsUsfFY6yyw0734XXvKsSdV9KbiHQ2hrBvzeZxtGMrlFaikVunCRizyLLWLMuOi4hoH+qy9F9sQid61gQIGkxwDAt85d/7Eau2/KzorFnZhgxArl7IiqJ67X6xjKkR3zur+Yp3V/mtVIehpPYIaAvPbcp2t4mQXl1I9J8yrQfEZOctLL1L4heDEVXnxvNihVLK6pivlVggp6SZCtjj9cduZGrYGsxsOCso1dqJQr7GCojfwvuLOoG0OYwEGuWVTZppxWxi/q1QgeHFhGKA5QUXlz7pS71oqpjYsTeViuHnfvlqb5TVYZpQ1haw=="}`)
+
+	err := json.Unmarshal(sejson, &dejson)
+	fmt.Printf("%v\n", dejson)
+	if err != nil {
+		t.Fatalf("%s", err)
+	}
+	resp := GetPasswordResult{gophercloud.Result{Body: dejson}}
+
+	pwd, err := resp.ExtractPassword(nil)
+	th.AssertNoErr(t, err)
+	th.AssertEquals(t, "PP8EnwPO9DhEc8+O/6CKAkPF379mKsUsfFY6yyw0734XXvKsSdV9KbiHQ2hrBvzeZxtGMrlFaikVunCRizyLLWLMuOi4hoH+qy9F9sQid61gQIGkxwDAt85d/7Eau2/KzorFnZhgxArl7IiqJ67X6xjKkR3zur+Yp3V/mtVIehpPYIaAvPbcp2t4mQXl1I9J8yrQfEZOctLL1L4heDEVXnxvNihVLK6pivlVggp6SZCtjj9cduZGrYGsxsOCso1dqJQr7GCojfwvuLOoG0OYwEGuWVTZppxWxi/q1QgeHFhGKA5QUXlz7pS71oqpjYsTeViuHnfvlqb5TVYZpQ1haw==", pwd)
+}
+
+// Ok - return decrypted password when private key is given.
+// Decrytion can be verified by:
+//   echo "<enc_pwd>" | base64 -D | openssl rsautl -decrypt -inkey <privateKey.pem>
+func TestExtractPassword_decrypted_pwd(t *testing.T) {
+
+	privateKey, err := ssh.ParseRawPrivateKey([]byte(`
+-----BEGIN RSA PRIVATE KEY-----
+MIIEpQIBAAKCAQEAo1ODZgwMVdTJYim9UYuYhowoPMhGEuV5IRZjcJ315r7RBSC+
+yEiBb1V+jhf+P8fzAyU35lkBzZGDr7E3jxSesbOuYT8cItQS4ErUnI1LGuqvMxwv
+X3GMyE/HmOcaiODF1XZN3Ur5pMJdVknnmczgUsW0hT98Udrh3MQn9WSuh/6LRy6+
+x1QsKHOCLFPnkhWa3LKyxmpQq/Gvhz+6NLe+gt8MFullA5mKQxBJ/K6laVHeaMlw
+JG3GCX0EZhRlvzoV8koIBKZtbKFolFr8ZtxBm3R5LvnyrtOvp22sa+xeItUT5kG1
+ZnbGNdK87oYW+VigEUfzT/+8R1i6E2QIXoeZiQIDAQABAoIBAQCVZ70IqbbTAW8j
+RAlyQh/J3Qal65LmkFJJKUDX8TfT1/Q/G6BKeMEmxm+Zrmsfj1pHI1HKftt+YEG1
+g4jOc09kQXkgbmnfll6aHPn3J+1vdwXD3GGdjrL5PrnYrngAhJWU2r8J0x8hT8ew
+OrUJZXhDX6XuSpAAFRmOKUZgXbSmo4X+LZX76ACnarselJt5FL724ECvpWJ7xxC4
+FMzvp4RqMmNFvv/Uq9lE/EmoSk4dviYyIZZ16DbDNyc9k/sGqCAMktCEwZ3EQm//
+S5bkNhgP6oUXjluWy53aPRgykEylgDWo5SSdSEyKnw/fciU0xdprA9JrBGIcTyHS
+/k2kgD4xAoGBANTkJ88Q0YrxX3fZNZVqcn00XKTxPGmxN5LRs7eV743q30AxK5Db
+QU8iwaAA1IKUWV5DLhgUTNsDCOPUPue4aOSBD3/sj+WEmvIhj7afDL5didkYHsqf
+fDnhFHq7y/3i57d428C7BwwR79pGWVyi7vH3pfu9A1iwl1aNOae+zvbVAoGBAMRm
+AmwQ9fJ3Qc44jysFK/yliLRGdShjkMMah5G3JlrelwfPtwPwEL2EHHhJB/C1acMs
+n6Q6RaoF6WNSZUY65ksQg7aPOYf2X0FTFwQJvwDJ4qlWjmq7w+tQ0AoGJG+dVUmQ
+zHZ/Y+HokSXzz9c4oevk4v/rMgAQ00WHrTdtIhnlAoGBALIJJ72D7CkNGHCq5qPQ
+xHQukPejgolFGhufYXM7YX3GmPMe67cVlTVv9Isxhoa5N0+cUPT0LR3PGOUm/4Bb
+eOT3hZXOqLwhvE6XgI8Rzd95bClwgXekDoh80dqeKMdmta961BQGlKskaPiacmsF
+G1yhZV70P9Mwwy8vpbLB4GUNAoGAbTwbjsWkNfa0qCF3J8NZoszjCvnBQfSW2J1R
+1+8ZKyNwt0yFi3Ajr3TibNiZzPzp1T9lj29FvfpJxA9Y+sXZvthxmcFxizix5GB1
+ha5yCNtA8VSOI7lJkAFDpL+j1lyYyjD6N9JE2KqEyKoh6J+8F7sXsqW7CqRRDfQX
+mKNfey0CgYEAxcEoNoADN2hRl7qY9rbQfVvQb3RkoQkdHhl9gpLFCcV32IP8R4xg
+09NbQK5OmgcIuZhLVNzTmUHJbabEGeXqIFIV0DsqECAt3WzbDyKQO23VJysFD46c
+KSde3I0ybDz7iS2EtceKB7m4C0slYd+oBkm4efuF00rCOKDwpFq45m0=
+-----END RSA PRIVATE KEY-----
+`))
+	if err != nil {
+		t.Fatalf("Error parsing private key: %s\n", err)
+	}
+
+	var dejson interface{}
+	sejson := []byte(`{"password":"PP8EnwPO9DhEc8+O/6CKAkPF379mKsUsfFY6yyw0734XXvKsSdV9KbiHQ2hrBvzeZxtGMrlFaikVunCRizyLLWLMuOi4hoH+qy9F9sQid61gQIGkxwDAt85d/7Eau2/KzorFnZhgxArl7IiqJ67X6xjKkR3zur+Yp3V/mtVIehpPYIaAvPbcp2t4mQXl1I9J8yrQfEZOctLL1L4heDEVXnxvNihVLK6pivlVggp6SZCtjj9cduZGrYGsxsOCso1dqJQr7GCojfwvuLOoG0OYwEGuWVTZppxWxi/q1QgeHFhGKA5QUXlz7pS71oqpjYsTeViuHnfvlqb5TVYZpQ1haw=="}`)
+
+	err = json.Unmarshal(sejson, &dejson)
+	fmt.Printf("%v\n", dejson)
+	if err != nil {
+		t.Fatalf("%s", err)
+	}
+	resp := GetPasswordResult{gophercloud.Result{Body: dejson}}
+
+	pwd, err := resp.ExtractPassword(privateKey.(*rsa.PrivateKey))
+	th.AssertNoErr(t, err)
+	th.AssertEquals(t, "ruZKK0tqxRfYm5t7lSJq", pwd)
+}
diff --git a/openstack/compute/v2/servers/urls.go b/openstack/compute/v2/servers/urls.go
index 8998354..d51fcbe 100644
--- a/openstack/compute/v2/servers/urls.go
+++ b/openstack/compute/v2/servers/urls.go
@@ -45,3 +45,7 @@
 func listAddressesByNetworkURL(client *gophercloud.ServiceClient, id, network string) string {
 	return client.ServiceURL("servers", id, "ips", network)
 }
+
+func passwordURL(client *gophercloud.ServiceClient, id string) string {
+	return client.ServiceURL("servers", id, "os-server-password")
+}
diff --git a/openstack/compute/v2/servers/urls_test.go b/openstack/compute/v2/servers/urls_test.go
index 17a1d28..9015d06 100644
--- a/openstack/compute/v2/servers/urls_test.go
+++ b/openstack/compute/v2/servers/urls_test.go
@@ -66,3 +66,9 @@
 	expected := endpoint + "servers/foo/metadata"
 	th.CheckEquals(t, expected, actual)
 }
+
+func TestPasswordURL(t *testing.T) {
+	actual := passwordURL(endpointClient(), "foo")
+	expected := endpoint + "servers/foo/os-server-password"
+	th.CheckEquals(t, expected, actual)
+}