diff --git a/doc/api_samples/all_extensions/extensions-get-resp.json b/doc/api_samples/all_extensions/extensions-get-resp.json index f2ed617bf216..f59ffc546280 100644 --- a/doc/api_samples/all_extensions/extensions-get-resp.json +++ b/doc/api_samples/all_extensions/extensions-get-resp.json @@ -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", diff --git a/doc/api_samples/all_extensions/extensions-get-resp.xml b/doc/api_samples/all_extensions/extensions-get-resp.xml index a17f415f5527..8e22e5ec95b9 100644 --- a/doc/api_samples/all_extensions/extensions-get-resp.xml +++ b/doc/api_samples/all_extensions/extensions-get-resp.xml @@ -146,6 +146,9 @@ Allow Admins to view server diagnostics through server action + + Server password support + Start/Stop instance compute API support diff --git a/doc/api_samples/os-server-password/get-password-resp.json b/doc/api_samples/os-server-password/get-password-resp.json new file mode 100644 index 000000000000..4becaf2921ca --- /dev/null +++ b/doc/api_samples/os-server-password/get-password-resp.json @@ -0,0 +1,3 @@ +{ + "password": "xlozO3wLCBRWAa2yDjCCVx8vwNPypxnypmRYDa/zErlQ+EzPe1S/Gz6nfmC52mOlOSCRuUOmG7kqqgejPof6M7bOezS387zjq4LSvvwp28zUknzy4YzfFGhnHAdai3TxUJ26pfQCYrq8UTzmKF2Bq8ioSEtVVzM0A96pDh8W2i7BOz6MdoiVyiev/I1K2LsuipfxSJR7Wdke4zNXJjHHP2RfYsVbZ/k9ANu+Nz4iIH8/7Cacud/pphH7EjrY6a4RZNrjQskrhKYed0YERpotyjYk1eDtRe72GrSiXteqCM4biaQ5w3ruS+AcX//PXk3uJ5kC7d67fPXaVz4WaQRYMg==" +} \ No newline at end of file diff --git a/doc/api_samples/os-server-password/get-password-resp.xml b/doc/api_samples/os-server-password/get-password-resp.xml new file mode 100644 index 000000000000..64b46a5712a5 --- /dev/null +++ b/doc/api_samples/os-server-password/get-password-resp.xml @@ -0,0 +1,2 @@ + +xlozO3wLCBRWAa2yDjCCVx8vwNPypxnypmRYDa/zErlQ+EzPe1S/Gz6nfmC52mOlOSCRuUOmG7kqqgejPof6M7bOezS387zjq4LSvvwp28zUknzy4YzfFGhnHAdai3TxUJ26pfQCYrq8UTzmKF2Bq8ioSEtVVzM0A96pDh8W2i7BOz6MdoiVyiev/I1K2LsuipfxSJR7Wdke4zNXJjHHP2RfYsVbZ/k9ANu+Nz4iIH8/7Cacud/pphH7EjrY6a4RZNrjQskrhKYed0YERpotyjYk1eDtRe72GrSiXteqCM4biaQ5w3ruS+AcX//PXk3uJ5kC7d67fPXaVz4WaQRYMg== \ No newline at end of file diff --git a/doc/api_samples/os-server-password/server-post-req.json b/doc/api_samples/os-server-password/server-post-req.json new file mode 100644 index 000000000000..d88eb4122223 --- /dev/null +++ b/doc/api_samples/os-server-password/server-post-req.json @@ -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==" + } + ] + } +} \ No newline at end of file diff --git a/doc/api_samples/os-server-password/server-post-req.xml b/doc/api_samples/os-server-password/server-post-req.xml new file mode 100644 index 000000000000..0a3c8bb5303d --- /dev/null +++ b/doc/api_samples/os-server-password/server-post-req.xml @@ -0,0 +1,19 @@ + + + + Apache1 + + + + ICAgICAgDQoiQSBjbG91ZCBkb2VzIG5vdCBrbm93IHdoeSBp + dCBtb3ZlcyBpbiBqdXN0IHN1Y2ggYSBkaXJlY3Rpb24gYW5k + IGF0IHN1Y2ggYSBzcGVlZC4uLkl0IGZlZWxzIGFuIGltcHVs + c2lvbi4uLnRoaXMgaXMgdGhlIHBsYWNlIHRvIGdvIG5vdy4g + QnV0IHRoZSBza3kga25vd3MgdGhlIHJlYXNvbnMgYW5kIHRo + ZSBwYXR0ZXJucyBiZWhpbmQgYWxsIGNsb3VkcywgYW5kIHlv + dSB3aWxsIGtub3csIHRvbywgd2hlbiB5b3UgbGlmdCB5b3Vy + c2VsZiBoaWdoIGVub3VnaCB0byBzZWUgYmV5b25kIGhvcml6 + b25zLiINCg0KLVJpY2hhcmQgQmFjaA== + + + \ No newline at end of file diff --git a/doc/api_samples/os-server-password/server-post-resp.json b/doc/api_samples/os-server-password/server-post-resp.json new file mode 100644 index 000000000000..0f477be49929 --- /dev/null +++ b/doc/api_samples/os-server-password/server-post-resp.json @@ -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" + } + ] + } +} \ No newline at end of file diff --git a/doc/api_samples/os-server-password/server-post-resp.xml b/doc/api_samples/os-server-password/server-post-resp.xml new file mode 100644 index 000000000000..cac50bc9b974 --- /dev/null +++ b/doc/api_samples/os-server-password/server-post-resp.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/etc/nova/policy.json b/etc/nova/policy.json index d7596deab77b..04766371eec4 100644 --- a/etc/nova/policy.json +++ b/etc/nova/policy.json @@ -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", diff --git a/nova/api/openstack/compute/contrib/server_password.py b/nova/api/openstack/compute/contrib/server_password.py new file mode 100644 index 000000000000..b4b2e04a552f --- /dev/null +++ b/nova/api/openstack/compute/contrib/server_password.py @@ -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 diff --git a/nova/tests/api/openstack/compute/contrib/test_server_password.py b/nova/tests/api/openstack/compute/contrib/test_server_password.py new file mode 100644 index 000000000000..600c4eda4031 --- /dev/null +++ b/nova/tests/api/openstack/compute/contrib/test_server_password.py @@ -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 '' diff --git a/nova/tests/api/openstack/compute/test_extensions.py b/nova/tests/api/openstack/compute/test_extensions.py index 66dac3febe8a..3df6b549b387 100644 --- a/nova/tests/api/openstack/compute/test_extensions.py +++ b/nova/tests/api/openstack/compute/test_extensions.py @@ -193,6 +193,7 @@ class ExtensionControllerTest(ExtensionTestCase): "SchedulerHints", "SecurityGroups", "ServerDiagnostics", + "ServerPassword", "ServerStartStop", "Services", "SimpleTenantUsage", diff --git a/nova/tests/fake_policy.py b/nova/tests/fake_policy.py index 80eb0874377f..58ff3289cd65 100644 --- a/nova/tests/fake_policy.py +++ b/nova/tests/fake_policy.py @@ -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": "", diff --git a/nova/tests/integrated/api_samples/all_extensions/extensions-get-resp.json.tpl b/nova/tests/integrated/api_samples/all_extensions/extensions-get-resp.json.tpl index 20f9044f9cb7..9395ba34981d 100644 --- a/nova/tests/integrated/api_samples/all_extensions/extensions-get-resp.json.tpl +++ b/nova/tests/integrated/api_samples/all_extensions/extensions-get-resp.json.tpl @@ -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", diff --git a/nova/tests/integrated/api_samples/all_extensions/extensions-get-resp.xml.tpl b/nova/tests/integrated/api_samples/all_extensions/extensions-get-resp.xml.tpl index 1669ff957237..cc022c0826a7 100644 --- a/nova/tests/integrated/api_samples/all_extensions/extensions-get-resp.xml.tpl +++ b/nova/tests/integrated/api_samples/all_extensions/extensions-get-resp.xml.tpl @@ -135,6 +135,9 @@ %(text)s + + %(text)s + %(text)s diff --git a/nova/tests/integrated/api_samples/os-server-password/get-password-resp.json.tpl b/nova/tests/integrated/api_samples/os-server-password/get-password-resp.json.tpl new file mode 100644 index 000000000000..026f15d46a30 --- /dev/null +++ b/nova/tests/integrated/api_samples/os-server-password/get-password-resp.json.tpl @@ -0,0 +1,3 @@ +{ + "password": "%(encrypted_password)s" +} diff --git a/nova/tests/integrated/api_samples/os-server-password/get-password-resp.xml.tpl b/nova/tests/integrated/api_samples/os-server-password/get-password-resp.xml.tpl new file mode 100644 index 000000000000..046eed30fbd4 --- /dev/null +++ b/nova/tests/integrated/api_samples/os-server-password/get-password-resp.xml.tpl @@ -0,0 +1,2 @@ + +%(encrypted_password)s diff --git a/nova/tests/integrated/api_samples/os-server-password/server-post-req.json.tpl b/nova/tests/integrated/api_samples/os-server-password/server-post-req.json.tpl new file mode 100644 index 000000000000..d3916d1aa68a --- /dev/null +++ b/nova/tests/integrated/api_samples/os-server-password/server-post-req.json.tpl @@ -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==" + } + ] + } +} diff --git a/nova/tests/integrated/api_samples/os-server-password/server-post-req.xml.tpl b/nova/tests/integrated/api_samples/os-server-password/server-post-req.xml.tpl new file mode 100644 index 000000000000..f92614984242 --- /dev/null +++ b/nova/tests/integrated/api_samples/os-server-password/server-post-req.xml.tpl @@ -0,0 +1,19 @@ + + + + Apache1 + + + + ICAgICAgDQoiQSBjbG91ZCBkb2VzIG5vdCBrbm93IHdoeSBp + dCBtb3ZlcyBpbiBqdXN0IHN1Y2ggYSBkaXJlY3Rpb24gYW5k + IGF0IHN1Y2ggYSBzcGVlZC4uLkl0IGZlZWxzIGFuIGltcHVs + c2lvbi4uLnRoaXMgaXMgdGhlIHBsYWNlIHRvIGdvIG5vdy4g + QnV0IHRoZSBza3kga25vd3MgdGhlIHJlYXNvbnMgYW5kIHRo + ZSBwYXR0ZXJucyBiZWhpbmQgYWxsIGNsb3VkcywgYW5kIHlv + dSB3aWxsIGtub3csIHRvbywgd2hlbiB5b3UgbGlmdCB5b3Vy + c2VsZiBoaWdoIGVub3VnaCB0byBzZWUgYmV5b25kIGhvcml6 + b25zLiINCg0KLVJpY2hhcmQgQmFjaA== + + + diff --git a/nova/tests/integrated/api_samples/os-server-password/server-post-resp.json.tpl b/nova/tests/integrated/api_samples/os-server-password/server-post-resp.json.tpl new file mode 100644 index 000000000000..d5f030c8730b --- /dev/null +++ b/nova/tests/integrated/api_samples/os-server-password/server-post-resp.json.tpl @@ -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" + } + ] + } +} diff --git a/nova/tests/integrated/api_samples/os-server-password/server-post-resp.xml.tpl b/nova/tests/integrated/api_samples/os-server-password/server-post-resp.xml.tpl new file mode 100644 index 000000000000..3bb13e69bd6d --- /dev/null +++ b/nova/tests/integrated/api_samples/os-server-password/server-post-resp.xml.tpl @@ -0,0 +1,6 @@ + + + + + + diff --git a/nova/tests/integrated/test_api_samples.py b/nova/tests/integrated/test_api_samples.py index f8bd522fe062..3788882dffb0 100644 --- a/nova/tests/integrated/test_api_samples.py +++ b/nova/tests/integrated/test_api_samples.py @@ -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")