Skip to content
This repository was archived by the owner on Aug 1, 2023. It is now read-only.

Commit 695436b

Browse files
committed
Merge pull request #534 from rickard-von-essen/os-server-password
Add Nova get-password support
2 parents f302fbf + 8b938da commit 695436b

File tree

7 files changed

+190
-4
lines changed

7 files changed

+190
-4
lines changed

openstack/compute/v2/servers/fixtures.go

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,12 @@ const SingleServerBody = `
235235
}
236236
`
237237

238+
const ServerPasswordBody = `
239+
{
240+
"password": "xlozO3wLCBRWAa2yDjCCVx8vwNPypxnypmRYDa/zErlQ+EzPe1S/Gz6nfmC52mOlOSCRuUOmG7kqqgejPof6M7bOezS387zjq4LSvvwp28zUknzy4YzfFGhnHAdai3TxUJ26pfQCYrq8UTzmKF2Bq8ioSEtVVzM0A96pDh8W2i7BOz6MdoiVyiev/I1K2LsuipfxSJR7Wdke4zNXJjHHP2RfYsVbZ/k9ANu+Nz4iIH8/7Cacud/pphH7EjrY6a4RZNrjQskrhKYed0YERpotyjYk1eDtRe72GrSiXteqCM4biaQ5w3ruS+AcX//PXk3uJ5kC7d67fPXaVz4WaQRYMg=="
241+
}
242+
`
243+
238244
var (
239245
// ServerHerp is a Server struct that should correspond to the first result in ServerListBody.
240246
ServerHerp = Server{
@@ -399,8 +405,8 @@ func HandleServerDeletionSuccessfully(t *testing.T) {
399405
})
400406
}
401407

402-
// HandleAdminPasswordChangeSuccessfully sets up the test server to respond to a server password
403-
// change request.
408+
// HandleServerForceDeletionSuccessfully sets up the test server to respond to a server force deletion
409+
// request.
404410
func HandleServerForceDeletionSuccessfully(t *testing.T) {
405411
th.Mux.HandleFunc("/servers/asdfasdfasdf/action", func(w http.ResponseWriter, r *http.Request) {
406412
th.TestMethod(t, r, "POST")
@@ -674,3 +680,13 @@ func HandleCreateServerImageSuccessfully(t *testing.T) {
674680
})
675681
}
676682

683+
// HandlePasswordGetSuccessfully sets up the test server to respond to a password Get request.
684+
func HandlePasswordGetSuccessfully(t *testing.T) {
685+
th.Mux.HandleFunc("/servers/1234asdf/os-server-password", func(w http.ResponseWriter, r *http.Request) {
686+
th.TestMethod(t, r, "GET")
687+
th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
688+
th.TestHeader(t, r, "Accept", "application/json")
689+
690+
fmt.Fprintf(w, ServerPasswordBody)
691+
})
692+
}

openstack/compute/v2/servers/requests.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -861,3 +861,12 @@ func IDFromName(client *gophercloud.ServiceClient, name string) (string, error)
861861
return "", fmt.Errorf("Found %d servers matching %s", serverCount, name)
862862
}
863863
}
864+
865+
// GetPassword makes a request against the nova API to get the encrypted administrative password.
866+
func GetPassword(client *gophercloud.ServiceClient, serverId string) GetPasswordResult {
867+
var res GetPasswordResult
868+
_, res.Err = client.Request("GET", passwordURL(client, serverId), gophercloud.RequestOpts{
869+
JSONResponse: &res.Body,
870+
})
871+
return res
872+
}

openstack/compute/v2/servers/requests_test.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,15 @@ func TestChangeServerAdminPassword(t *testing.T) {
124124
th.AssertNoErr(t, res.Err)
125125
}
126126

127+
func TestGetPassword(t *testing.T) {
128+
th.SetupHTTP()
129+
defer th.TeardownHTTP()
130+
HandlePasswordGetSuccessfully(t)
131+
132+
res := GetPassword(client.ServiceClient(), "1234asdf")
133+
th.AssertNoErr(t, res.Err)
134+
}
135+
127136
func TestRebootServer(t *testing.T) {
128137
th.SetupHTTP()
129138
defer th.TeardownHTTP()

openstack/compute/v2/servers/results.go

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
package servers
22

33
import (
4-
"reflect"
4+
"crypto/rsa"
5+
"encoding/base64"
56
"fmt"
6-
"path"
77
"net/url"
8+
"path"
9+
"reflect"
810

911
"github.com/mitchellh/mapstructure"
1012
"github.com/rackspace/gophercloud"
@@ -82,6 +84,47 @@ type CreateImageResult struct {
8284
gophercloud.Result
8385
}
8486

87+
// GetPasswordResult represent the result of a get os-server-password operation.
88+
type GetPasswordResult struct {
89+
gophercloud.Result
90+
}
91+
92+
// ExtractPassword gets the encrypted password.
93+
// If privateKey != nil the password is decrypted with the private key.
94+
// If privateKey == nil the encrypted password is returned and can be decrypted with:
95+
// echo '<pwd>' | base64 -D | openssl rsautl -decrypt -inkey <private_key>
96+
func (r GetPasswordResult) ExtractPassword(privateKey *rsa.PrivateKey) (string, error) {
97+
98+
if r.Err != nil {
99+
return "", r.Err
100+
}
101+
102+
var response struct {
103+
Password string `mapstructure:"password"`
104+
}
105+
106+
err := mapstructure.Decode(r.Body, &response)
107+
if err == nil && privateKey != nil && response.Password != "" {
108+
return decryptPassword(response.Password, privateKey)
109+
}
110+
return response.Password, err
111+
}
112+
113+
func decryptPassword(encryptedPassword string, privateKey *rsa.PrivateKey) (string, error) {
114+
b64EncryptedPassword := make([]byte, base64.StdEncoding.DecodedLen(len(encryptedPassword)))
115+
116+
n, err := base64.StdEncoding.Decode(b64EncryptedPassword, []byte(encryptedPassword))
117+
if err != nil {
118+
return "", fmt.Errorf("Failed to base64 decode encrypted password: %s", err)
119+
}
120+
password, err := rsa.DecryptPKCS1v15(nil, privateKey, b64EncryptedPassword[0:n])
121+
if err != nil {
122+
return "", fmt.Errorf("Failed to decrypt password: %s", err)
123+
}
124+
125+
return string(password), nil
126+
}
127+
85128
// ExtractImageID gets the ID of the newly created server image from the header
86129
func (res CreateImageResult) ExtractImageID() (string, error) {
87130
if res.Err != nil {
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
// +build fixtures
2+
3+
package servers
4+
5+
import (
6+
"crypto/rsa"
7+
"encoding/json"
8+
"fmt"
9+
"testing"
10+
11+
"github.com/rackspace/gophercloud"
12+
th "github.com/rackspace/gophercloud/testhelper"
13+
"golang.org/x/crypto/ssh"
14+
)
15+
16+
// Fail - No password in JSON.
17+
func TestExtractPassword_no_pwd_data(t *testing.T) {
18+
19+
var dejson interface{}
20+
err := json.Unmarshal([]byte(`{ "Crappy data": ".-.-." }`), &dejson)
21+
if err != nil {
22+
t.Fatalf("%s", err)
23+
}
24+
resp := GetPasswordResult{gophercloud.Result{Body: dejson}}
25+
26+
pwd, err := resp.ExtractPassword(nil)
27+
th.AssertEquals(t, pwd, "")
28+
}
29+
30+
// Ok - return encrypted password when no private key is given.
31+
func TestExtractPassword_encrypted_pwd(t *testing.T) {
32+
33+
var dejson interface{}
34+
sejson := []byte(`{"password":"PP8EnwPO9DhEc8+O/6CKAkPF379mKsUsfFY6yyw0734XXvKsSdV9KbiHQ2hrBvzeZxtGMrlFaikVunCRizyLLWLMuOi4hoH+qy9F9sQid61gQIGkxwDAt85d/7Eau2/KzorFnZhgxArl7IiqJ67X6xjKkR3zur+Yp3V/mtVIehpPYIaAvPbcp2t4mQXl1I9J8yrQfEZOctLL1L4heDEVXnxvNihVLK6pivlVggp6SZCtjj9cduZGrYGsxsOCso1dqJQr7GCojfwvuLOoG0OYwEGuWVTZppxWxi/q1QgeHFhGKA5QUXlz7pS71oqpjYsTeViuHnfvlqb5TVYZpQ1haw=="}`)
35+
36+
err := json.Unmarshal(sejson, &dejson)
37+
fmt.Printf("%v\n", dejson)
38+
if err != nil {
39+
t.Fatalf("%s", err)
40+
}
41+
resp := GetPasswordResult{gophercloud.Result{Body: dejson}}
42+
43+
pwd, err := resp.ExtractPassword(nil)
44+
th.AssertNoErr(t, err)
45+
th.AssertEquals(t, "PP8EnwPO9DhEc8+O/6CKAkPF379mKsUsfFY6yyw0734XXvKsSdV9KbiHQ2hrBvzeZxtGMrlFaikVunCRizyLLWLMuOi4hoH+qy9F9sQid61gQIGkxwDAt85d/7Eau2/KzorFnZhgxArl7IiqJ67X6xjKkR3zur+Yp3V/mtVIehpPYIaAvPbcp2t4mQXl1I9J8yrQfEZOctLL1L4heDEVXnxvNihVLK6pivlVggp6SZCtjj9cduZGrYGsxsOCso1dqJQr7GCojfwvuLOoG0OYwEGuWVTZppxWxi/q1QgeHFhGKA5QUXlz7pS71oqpjYsTeViuHnfvlqb5TVYZpQ1haw==", pwd)
46+
}
47+
48+
// Ok - return decrypted password when private key is given.
49+
// Decrytion can be verified by:
50+
// echo "<enc_pwd>" | base64 -D | openssl rsautl -decrypt -inkey <privateKey.pem>
51+
func TestExtractPassword_decrypted_pwd(t *testing.T) {
52+
53+
privateKey, err := ssh.ParseRawPrivateKey([]byte(`
54+
-----BEGIN RSA PRIVATE KEY-----
55+
MIIEpQIBAAKCAQEAo1ODZgwMVdTJYim9UYuYhowoPMhGEuV5IRZjcJ315r7RBSC+
56+
yEiBb1V+jhf+P8fzAyU35lkBzZGDr7E3jxSesbOuYT8cItQS4ErUnI1LGuqvMxwv
57+
X3GMyE/HmOcaiODF1XZN3Ur5pMJdVknnmczgUsW0hT98Udrh3MQn9WSuh/6LRy6+
58+
x1QsKHOCLFPnkhWa3LKyxmpQq/Gvhz+6NLe+gt8MFullA5mKQxBJ/K6laVHeaMlw
59+
JG3GCX0EZhRlvzoV8koIBKZtbKFolFr8ZtxBm3R5LvnyrtOvp22sa+xeItUT5kG1
60+
ZnbGNdK87oYW+VigEUfzT/+8R1i6E2QIXoeZiQIDAQABAoIBAQCVZ70IqbbTAW8j
61+
RAlyQh/J3Qal65LmkFJJKUDX8TfT1/Q/G6BKeMEmxm+Zrmsfj1pHI1HKftt+YEG1
62+
g4jOc09kQXkgbmnfll6aHPn3J+1vdwXD3GGdjrL5PrnYrngAhJWU2r8J0x8hT8ew
63+
OrUJZXhDX6XuSpAAFRmOKUZgXbSmo4X+LZX76ACnarselJt5FL724ECvpWJ7xxC4
64+
FMzvp4RqMmNFvv/Uq9lE/EmoSk4dviYyIZZ16DbDNyc9k/sGqCAMktCEwZ3EQm//
65+
S5bkNhgP6oUXjluWy53aPRgykEylgDWo5SSdSEyKnw/fciU0xdprA9JrBGIcTyHS
66+
/k2kgD4xAoGBANTkJ88Q0YrxX3fZNZVqcn00XKTxPGmxN5LRs7eV743q30AxK5Db
67+
QU8iwaAA1IKUWV5DLhgUTNsDCOPUPue4aOSBD3/sj+WEmvIhj7afDL5didkYHsqf
68+
fDnhFHq7y/3i57d428C7BwwR79pGWVyi7vH3pfu9A1iwl1aNOae+zvbVAoGBAMRm
69+
AmwQ9fJ3Qc44jysFK/yliLRGdShjkMMah5G3JlrelwfPtwPwEL2EHHhJB/C1acMs
70+
n6Q6RaoF6WNSZUY65ksQg7aPOYf2X0FTFwQJvwDJ4qlWjmq7w+tQ0AoGJG+dVUmQ
71+
zHZ/Y+HokSXzz9c4oevk4v/rMgAQ00WHrTdtIhnlAoGBALIJJ72D7CkNGHCq5qPQ
72+
xHQukPejgolFGhufYXM7YX3GmPMe67cVlTVv9Isxhoa5N0+cUPT0LR3PGOUm/4Bb
73+
eOT3hZXOqLwhvE6XgI8Rzd95bClwgXekDoh80dqeKMdmta961BQGlKskaPiacmsF
74+
G1yhZV70P9Mwwy8vpbLB4GUNAoGAbTwbjsWkNfa0qCF3J8NZoszjCvnBQfSW2J1R
75+
1+8ZKyNwt0yFi3Ajr3TibNiZzPzp1T9lj29FvfpJxA9Y+sXZvthxmcFxizix5GB1
76+
ha5yCNtA8VSOI7lJkAFDpL+j1lyYyjD6N9JE2KqEyKoh6J+8F7sXsqW7CqRRDfQX
77+
mKNfey0CgYEAxcEoNoADN2hRl7qY9rbQfVvQb3RkoQkdHhl9gpLFCcV32IP8R4xg
78+
09NbQK5OmgcIuZhLVNzTmUHJbabEGeXqIFIV0DsqECAt3WzbDyKQO23VJysFD46c
79+
KSde3I0ybDz7iS2EtceKB7m4C0slYd+oBkm4efuF00rCOKDwpFq45m0=
80+
-----END RSA PRIVATE KEY-----
81+
`))
82+
if err != nil {
83+
t.Fatalf("Error parsing private key: %s\n", err)
84+
}
85+
86+
var dejson interface{}
87+
sejson := []byte(`{"password":"PP8EnwPO9DhEc8+O/6CKAkPF379mKsUsfFY6yyw0734XXvKsSdV9KbiHQ2hrBvzeZxtGMrlFaikVunCRizyLLWLMuOi4hoH+qy9F9sQid61gQIGkxwDAt85d/7Eau2/KzorFnZhgxArl7IiqJ67X6xjKkR3zur+Yp3V/mtVIehpPYIaAvPbcp2t4mQXl1I9J8yrQfEZOctLL1L4heDEVXnxvNihVLK6pivlVggp6SZCtjj9cduZGrYGsxsOCso1dqJQr7GCojfwvuLOoG0OYwEGuWVTZppxWxi/q1QgeHFhGKA5QUXlz7pS71oqpjYsTeViuHnfvlqb5TVYZpQ1haw=="}`)
88+
89+
err = json.Unmarshal(sejson, &dejson)
90+
fmt.Printf("%v\n", dejson)
91+
if err != nil {
92+
t.Fatalf("%s", err)
93+
}
94+
resp := GetPasswordResult{gophercloud.Result{Body: dejson}}
95+
96+
pwd, err := resp.ExtractPassword(privateKey.(*rsa.PrivateKey))
97+
th.AssertNoErr(t, err)
98+
th.AssertEquals(t, "ruZKK0tqxRfYm5t7lSJq", pwd)
99+
}

openstack/compute/v2/servers/urls.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,3 +45,7 @@ func listAddressesURL(client *gophercloud.ServiceClient, id string) string {
4545
func listAddressesByNetworkURL(client *gophercloud.ServiceClient, id, network string) string {
4646
return client.ServiceURL("servers", id, "ips", network)
4747
}
48+
49+
func passwordURL(client *gophercloud.ServiceClient, id string) string {
50+
return client.ServiceURL("servers", id, "os-server-password")
51+
}

openstack/compute/v2/servers/urls_test.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,3 +66,9 @@ func TestMetadataURL(t *testing.T) {
6666
expected := endpoint + "servers/foo/metadata"
6767
th.CheckEquals(t, expected, actual)
6868
}
69+
70+
func TestPasswordURL(t *testing.T) {
71+
actual := passwordURL(endpointClient(), "foo")
72+
expected := endpoint + "servers/foo/os-server-password"
73+
th.CheckEquals(t, expected, actual)
74+
}

0 commit comments

Comments
 (0)