Add api extension to get and reset password

Password is stored in system_instance_metadata as four items with the
keys 'password_0' through 'password_3'. The extension adds a resource
endpoint with two methods:

GET v2/servers/<uuid>/os-server-password  # get_password
DELETE v2/servers/<uuid>/os-server-password # reset_password

get_password retuns:

JSON: {"password": "xxx"}
XML: <?xml version='1.0' encoding='UTF-8'?><password>xxx</password>

Note that this is intended to be an encrypted password set by the
guest. Includes api tests for json and xml. Also includes api samples.

Part of blueprint get-password

Change-Id: I6c03f9c3bc8a2c70403bbb3e57917ab9522d75bd
This commit is contained in:
Vishvananda Ishaya 2012-11-30 15:49:17 -08:00
parent aa2dea35c6
commit 285994be59
22 changed files with 356 additions and 0 deletions

View File

@ -352,6 +352,14 @@
"namespace": "http://docs.openstack.org/compute/ext/server-diagnostics/api/v1.1",
"updated": "2011-12-21T00:00:00+00:00"
},
{
"alias": "os-server-password",
"description": "Server password support",
"links": [],
"name": "ServerPassword",
"namespace": "http://docs.openstack.org/compute/ext/server-password/api/v2",
"updated": "2012-11-29T00:00:00+00:00"
},
{
"alias": "os-server-start-stop",
"description": "Start/Stop instance compute API support",

View File

@ -146,6 +146,9 @@
<extension alias="os-server-diagnostics" updated="2011-12-21T00:00:00+00:00" namespace="http://docs.openstack.org/compute/ext/server-diagnostics/api/v1.1" name="ServerDiagnostics">
<description>Allow Admins to view server diagnostics through server action</description>
</extension>
<extension alias="os-server-password" updated="2012-11-29T00:00:00+00:00" namespace="http://docs.openstack.org/compute/ext/server-password/api/v2" name="ServerPassword">
<description>Server password support</description>
</extension>
<extension alias="os-server-start-stop" updated="2012-01-23T00:00:00+00:00" namespace="http://docs.openstack.org/compute/ext/servers/api/v1.1" name="ServerStartStop">
<description>Start/Stop instance compute API support</description>
</extension>

View File

@ -0,0 +1,3 @@
{
"password": "xlozO3wLCBRWAa2yDjCCVx8vwNPypxnypmRYDa/zErlQ+EzPe1S/Gz6nfmC52mOlOSCRuUOmG7kqqgejPof6M7bOezS387zjq4LSvvwp28zUknzy4YzfFGhnHAdai3TxUJ26pfQCYrq8UTzmKF2Bq8ioSEtVVzM0A96pDh8W2i7BOz6MdoiVyiev/I1K2LsuipfxSJR7Wdke4zNXJjHHP2RfYsVbZ/k9ANu+Nz4iIH8/7Cacud/pphH7EjrY6a4RZNrjQskrhKYed0YERpotyjYk1eDtRe72GrSiXteqCM4biaQ5w3ruS+AcX//PXk3uJ5kC7d67fPXaVz4WaQRYMg=="
}

View File

@ -0,0 +1,2 @@
<?xml version='1.0' encoding='UTF-8'?>
<password>xlozO3wLCBRWAa2yDjCCVx8vwNPypxnypmRYDa/zErlQ+EzPe1S/Gz6nfmC52mOlOSCRuUOmG7kqqgejPof6M7bOezS387zjq4LSvvwp28zUknzy4YzfFGhnHAdai3TxUJ26pfQCYrq8UTzmKF2Bq8ioSEtVVzM0A96pDh8W2i7BOz6MdoiVyiev/I1K2LsuipfxSJR7Wdke4zNXJjHHP2RfYsVbZ/k9ANu+Nz4iIH8/7Cacud/pphH7EjrY6a4RZNrjQskrhKYed0YERpotyjYk1eDtRe72GrSiXteqCM4biaQ5w3ruS+AcX//PXk3uJ5kC7d67fPXaVz4WaQRYMg==</password>

View File

@ -0,0 +1,16 @@
{
"server" : {
"name" : "new-server-test",
"imageRef" : "http://openstack.example.com/openstack/images/70a599e0-31e7-49b7-b260-868f441e862b",
"flavorRef" : "http://openstack.example.com/openstack/flavors/1",
"metadata" : {
"My Server Name" : "Apache1"
},
"personality" : [
{
"path" : "/etc/banner.txt",
"contents" : "ICAgICAgDQoiQSBjbG91ZCBkb2VzIG5vdCBrbm93IHdoeSBpdCBtb3ZlcyBpbiBqdXN0IHN1Y2ggYSBkaXJlY3Rpb24gYW5kIGF0IHN1Y2ggYSBzcGVlZC4uLkl0IGZlZWxzIGFuIGltcHVsc2lvbi4uLnRoaXMgaXMgdGhlIHBsYWNlIHRvIGdvIG5vdy4gQnV0IHRoZSBza3kga25vd3MgdGhlIHJlYXNvbnMgYW5kIHRoZSBwYXR0ZXJucyBiZWhpbmQgYWxsIGNsb3VkcywgYW5kIHlvdSB3aWxsIGtub3csIHRvbywgd2hlbiB5b3UgbGlmdCB5b3Vyc2VsZiBoaWdoIGVub3VnaCB0byBzZWUgYmV5b25kIGhvcml6b25zLiINCg0KLVJpY2hhcmQgQmFjaA=="
}
]
}
}

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<server xmlns="http://docs.openstack.org/compute/api/v1.1" imageRef="http://openstack.example.com/openstack/images/70a599e0-31e7-49b7-b260-868f441e862b" flavorRef="http://openstack.example.com/openstack/flavors/1" name="new-server-test">
<metadata>
<meta key="My Server Name">Apache1</meta>
</metadata>
<personality>
<file path="/etc/banner.txt">
ICAgICAgDQoiQSBjbG91ZCBkb2VzIG5vdCBrbm93IHdoeSBp
dCBtb3ZlcyBpbiBqdXN0IHN1Y2ggYSBkaXJlY3Rpb24gYW5k
IGF0IHN1Y2ggYSBzcGVlZC4uLkl0IGZlZWxzIGFuIGltcHVs
c2lvbi4uLnRoaXMgaXMgdGhlIHBsYWNlIHRvIGdvIG5vdy4g
QnV0IHRoZSBza3kga25vd3MgdGhlIHJlYXNvbnMgYW5kIHRo
ZSBwYXR0ZXJucyBiZWhpbmQgYWxsIGNsb3VkcywgYW5kIHlv
dSB3aWxsIGtub3csIHRvbywgd2hlbiB5b3UgbGlmdCB5b3Vy
c2VsZiBoaWdoIGVub3VnaCB0byBzZWUgYmV5b25kIGhvcml6
b25zLiINCg0KLVJpY2hhcmQgQmFjaA==
</file>
</personality>
</server>

View File

@ -0,0 +1,16 @@
{
"server": {
"adminPass": "78AtBtuxTqZV",
"id": "66fd64e1-de18-4506-bfb6-b5e73ef78a43",
"links": [
{
"href": "http://openstack.example.com/v2/openstack/servers/66fd64e1-de18-4506-bfb6-b5e73ef78a43",
"rel": "self"
},
{
"href": "http://openstack.example.com/openstack/servers/66fd64e1-de18-4506-bfb6-b5e73ef78a43",
"rel": "bookmark"
}
]
}
}

View File

@ -0,0 +1,6 @@
<?xml version='1.0' encoding='UTF-8'?>
<server xmlns:atom="http://www.w3.org/2005/Atom" xmlns="http://docs.openstack.org/compute/api/v1.1" id="b68e3354-0b1a-4e92-a664-8b332cff27f5" adminPass="sLV7uLzmgoHu">
<metadata/>
<atom:link href="http://openstack.example.com/v2/openstack/servers/b68e3354-0b1a-4e92-a664-8b332cff27f5" rel="self"/>
<atom:link href="http://openstack.example.com/openstack/servers/b68e3354-0b1a-4e92-a664-8b332cff27f5" rel="bookmark"/>
</server>

View File

@ -73,6 +73,7 @@
"compute_extension:rescue": "",
"compute_extension:security_groups": "",
"compute_extension:server_diagnostics": "rule:admin_api",
"compute_extension:server_password": "",
"compute_extension:services": "rule:admin_api",
"compute_extension:simple_tenant_usage:show": "rule:admin_or_owner",
"compute_extension:simple_tenant_usage:list": "rule:admin_api",

View File

@ -0,0 +1,87 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright (c) 2012 Nebula, Inc.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""The server password extension."""
import webob
from nova.api.metadata import password
from nova.api.openstack import extensions
from nova.api.openstack import wsgi
from nova.api.openstack import xmlutil
from nova import compute
from nova import exception
authorize = extensions.extension_authorizer('compute', 'server_password')
class ServerPasswordTemplate(xmlutil.TemplateBuilder):
def construct(self):
root = xmlutil.TemplateElement('password', selector='password')
root.text = unicode
return xmlutil.MasterTemplate(root, 1)
class ServerPasswordController(object):
"""The flavor access API controller for the OpenStack API."""
def __init__(self):
self.compute_api = compute.API()
def _get_instance(self, context, server_id):
try:
return self.compute_api.get(context, server_id)
except exception.InstanceNotFound as exp:
raise webob.exc.HTTPNotFound(explanation=unicode(exp))
@wsgi.serializers(xml=ServerPasswordTemplate)
def index(self, req, server_id):
context = req.environ['nova.context']
authorize(context)
instance = self._get_instance(context, server_id)
passw = password.extract_password(instance)
return {'password': passw or ''}
@wsgi.response(204)
def delete(self, req, server_id):
context = req.environ['nova.context']
authorize(context)
instance = self._get_instance(context, server_id)
password.set_password(context, instance['uuid'], None)
class Server_password(extensions.ExtensionDescriptor):
"""Server password support"""
name = "ServerPassword"
alias = "os-server-password"
namespace = ("http://docs.openstack.org/compute/ext/"
"server-password/api/v2")
updated = "2012-11-29T00:00:00+00:00"
def get_resources(self):
resources = []
res = extensions.ResourceExtension(
'os-server-password',
controller=ServerPasswordController(),
collection_actions={'delete': 'DELETE'},
parent=dict(member_name='server', collection_name='servers'))
resources.append(res)
return resources

View File

@ -0,0 +1,86 @@
# Copyright 2012 Nebula, Inc.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from lxml import etree
import webob
from nova.api.metadata import password
from nova import compute
from nova.openstack.common import cfg
from nova.openstack.common import jsonutils
from nova import test
from nova.tests.api.openstack import fakes
CONF = cfg.CONF
CONF.import_opt('osapi_compute_ext_list', 'nova.api.openstack.compute.contrib')
class ServerPasswordTest(test.TestCase):
content_type = 'application/json'
def setUp(self):
super(ServerPasswordTest, self).setUp()
fakes.stub_out_nw_api(self.stubs)
self.stubs.Set(compute.api.API, 'get', lambda *a, **kw: {'uuid': ''})
self.password = 'fakepass'
def fake_extract_password(instance):
return self.password
def fake_set_password(context, instance_uuid, password):
self.password = password
self.stubs.Set(password, 'extract_password', fake_extract_password)
self.stubs.Set(password, 'set_password', fake_set_password)
self.flags(
osapi_compute_extension=[
'nova.api.openstack.compute.contrib.select_extensions'],
osapi_compute_ext_list=['Server_password'])
def _make_request(self, url, method='GET'):
req = webob.Request.blank(url)
req.headers['Accept'] = self.content_type
req.method = method
res = req.get_response(
fakes.wsgi_app(init_only=('servers', 'os-server-password')))
return res
def _get_pass(self, body):
return jsonutils.loads(body).get('password')
def test_get_password(self):
url = '/v2/fake/servers/fake/os-server-password'
res = self._make_request(url)
self.assertEqual(res.status_int, 200)
self.assertEqual(self._get_pass(res.body), 'fakepass')
def test_reset_password(self):
url = '/v2/fake/servers/fake/os-server-password'
res = self._make_request(url, 'DELETE')
self.assertEqual(res.status_int, 204)
res = self._make_request(url)
self.assertEqual(res.status_int, 200)
self.assertEqual(self._get_pass(res.body), '')
class ServerPasswordXmlTest(ServerPasswordTest):
content_type = 'application/xml'
def _get_pass(self, body):
# NOTE(vish): first element is password
return etree.XML(body).text or ''

View File

@ -193,6 +193,7 @@ class ExtensionControllerTest(ExtensionTestCase):
"SchedulerHints",
"SecurityGroups",
"ServerDiagnostics",
"ServerPassword",
"ServerStartStop",
"Services",
"SimpleTenantUsage",

View File

@ -145,6 +145,7 @@ policy_data = """
"compute_extension:rescue": "",
"compute_extension:security_groups": "",
"compute_extension:server_diagnostics": "",
"compute_extension:server_password": "",
"compute_extension:services": "",
"compute_extension:simple_tenant_usage:show": "",
"compute_extension:simple_tenant_usage:list": "",

View File

@ -360,6 +360,14 @@
"namespace": "http://docs.openstack.org/compute/ext/server-diagnostics/api/v1.1",
"updated": "%(timestamp)s"
},
{
"alias": "os-server-password",
"description": "%(text)s",
"links": [],
"name": "ServerPassword",
"namespace": "http://docs.openstack.org/compute/ext/server-password/api/v2",
"updated": "2012-11-29T00:00:00+00:00"
},
{
"alias": "os-server-start-stop",
"description": "%(text)s",

View File

@ -135,6 +135,9 @@
<extension alias="os-server-diagnostics" updated="%(timestamp)s" namespace="http://docs.openstack.org/compute/ext/server-diagnostics/api/v1.1" name="ServerDiagnostics">
<description>%(text)s</description>
</extension>
<extension alias="os-server-password" updated="%(timestamp)s" namespace="http://docs.openstack.org/compute/ext/server-password/api/v2" name="ServerPassword">
<description>%(text)s</description>
</extension>
<extension alias="os-server-start-stop" updated="%(timestamp)s" namespace="http://docs.openstack.org/compute/ext/servers/api/v1.1" name="ServerStartStop">
<description>%(text)s</description>
</extension>

View File

@ -0,0 +1,3 @@
{
"password": "%(encrypted_password)s"
}

View File

@ -0,0 +1,2 @@
<?xml version='1.0' encoding='UTF-8'?>
<password>%(encrypted_password)s</password>

View File

@ -0,0 +1,16 @@
{
"server" : {
"name" : "new-server-test",
"imageRef" : "%(host)s/openstack/images/%(image_id)s",
"flavorRef" : "%(host)s/openstack/flavors/1",
"metadata" : {
"My Server Name" : "Apache1"
},
"personality" : [
{
"path" : "/etc/banner.txt",
"contents" : "ICAgICAgDQoiQSBjbG91ZCBkb2VzIG5vdCBrbm93IHdoeSBpdCBtb3ZlcyBpbiBqdXN0IHN1Y2ggYSBkaXJlY3Rpb24gYW5kIGF0IHN1Y2ggYSBzcGVlZC4uLkl0IGZlZWxzIGFuIGltcHVsc2lvbi4uLnRoaXMgaXMgdGhlIHBsYWNlIHRvIGdvIG5vdy4gQnV0IHRoZSBza3kga25vd3MgdGhlIHJlYXNvbnMgYW5kIHRoZSBwYXR0ZXJucyBiZWhpbmQgYWxsIGNsb3VkcywgYW5kIHlvdSB3aWxsIGtub3csIHRvbywgd2hlbiB5b3UgbGlmdCB5b3Vyc2VsZiBoaWdoIGVub3VnaCB0byBzZWUgYmV5b25kIGhvcml6b25zLiINCg0KLVJpY2hhcmQgQmFjaA=="
}
]
}
}

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<server xmlns="http://docs.openstack.org/compute/api/v1.1" imageRef="%(host)s/openstack/images/%(image_id)s" flavorRef="%(host)s/openstack/flavors/1" name="new-server-test">
<metadata>
<meta key="My Server Name">Apache1</meta>
</metadata>
<personality>
<file path="/etc/banner.txt">
ICAgICAgDQoiQSBjbG91ZCBkb2VzIG5vdCBrbm93IHdoeSBp
dCBtb3ZlcyBpbiBqdXN0IHN1Y2ggYSBkaXJlY3Rpb24gYW5k
IGF0IHN1Y2ggYSBzcGVlZC4uLkl0IGZlZWxzIGFuIGltcHVs
c2lvbi4uLnRoaXMgaXMgdGhlIHBsYWNlIHRvIGdvIG5vdy4g
QnV0IHRoZSBza3kga25vd3MgdGhlIHJlYXNvbnMgYW5kIHRo
ZSBwYXR0ZXJucyBiZWhpbmQgYWxsIGNsb3VkcywgYW5kIHlv
dSB3aWxsIGtub3csIHRvbywgd2hlbiB5b3UgbGlmdCB5b3Vy
c2VsZiBoaWdoIGVub3VnaCB0byBzZWUgYmV5b25kIGhvcml6
b25zLiINCg0KLVJpY2hhcmQgQmFjaA==
</file>
</personality>
</server>

View File

@ -0,0 +1,16 @@
{
"server": {
"adminPass": "%(password)s",
"id": "%(id)s",
"links": [
{
"href": "%(host)s/v2/openstack/servers/%(uuid)s",
"rel": "self"
},
{
"href": "%(host)s/openstack/servers/%(uuid)s",
"rel": "bookmark"
}
]
}
}

View File

@ -0,0 +1,6 @@
<?xml version='1.0' encoding='UTF-8'?>
<server xmlns:atom="http://www.w3.org/2005/Atom" xmlns="http://docs.openstack.org/compute/api/v1.1" id="%(id)s" adminPass="%(password)s">
<metadata/>
<atom:link href="%(host)s/v2/openstack/servers/%(uuid)s" rel="self"/>
<atom:link href="%(host)s/openstack/servers/%(uuid)s" rel="bookmark"/>
</server>

View File

@ -24,6 +24,7 @@ import uuid as uuid_lib
from coverage import coverage
from lxml import etree
from nova.api.metadata import password
from nova.api.openstack.compute.contrib import coverage_ext
# Import extensions to pull in osapi_compute_extension CONF option used below.
from nova.api.openstack.compute import extensions
@ -2148,6 +2149,39 @@ class FlavorManageSampleXmlTests(FlavorManageSampleJsonTests):
ctype = "xml"
class ServerPasswordSampleJsonTests(ServersSampleBase):
extension_name = ("nova.api.openstack.compute.contrib.server_password."
"Server_password")
def test_get_password(self):
# Mock password since there is no api to set it
def fake_ext_password(*args, **kwargs):
return ("xlozO3wLCBRWAa2yDjCCVx8vwNPypxnypmRYDa/zErlQ+EzPe1S/"
"Gz6nfmC52mOlOSCRuUOmG7kqqgejPof6M7bOezS387zjq4LSvvwp"
"28zUknzy4YzfFGhnHAdai3TxUJ26pfQCYrq8UTzmKF2Bq8ioSEtV"
"VzM0A96pDh8W2i7BOz6MdoiVyiev/I1K2LsuipfxSJR7Wdke4zNX"
"JjHHP2RfYsVbZ/k9ANu+Nz4iIH8/7Cacud/pphH7EjrY6a4RZNrj"
"QskrhKYed0YERpotyjYk1eDtRe72GrSiXteqCM4biaQ5w3ruS+Ac"
"X//PXk3uJ5kC7d67fPXaVz4WaQRYMg==")
self.stubs.Set(password, "extract_password", fake_ext_password)
uuid = self._post_server()
response = self._do_get('servers/%s/os-server-password' % uuid)
self.assertEqual(response.status, 200)
subs = self._get_regexes()
subs['encrypted_password'] = fake_ext_password().replace('+', '\\+')
return self._verify_response('get-password-resp', subs, response)
def test_reset_password(self):
uuid = self._post_server()
response = self._do_delete('servers/%s/os-server-password' % uuid)
self.assertEqual(response.status, 204)
class ServerPasswordSampleXmlTests(ServerPasswordSampleJsonTests):
ctype = "xml"
class DiskConfigJsonTest(ServersSampleBase):
extension_name = ("nova.api.openstack.compute.contrib.disk_config."
"Disk_config")