Add Nova get-password support
Add support to get a encrypted administrative password for a server
through a GET on: /v2.1/{tenant_id}/servers/{server_id}/os-server-password
optionally decrypting the password if a private key is supplied.
The same operation with OpenStack CLI is done with:
nova get-password <server_id> [private_key.pem]
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..b995053 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 agains 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/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)
+}