From 1a3e02f1cd2892b753ac3833e1a482c5776c36d6 Mon Sep 17 00:00:00 2001 From: Chris Yeoh Date: Mon, 13 May 2013 15:57:04 +0930 Subject: Ports consoles API to v3 API Ports the core consoles API functionality to the V3 API as a plugin along with the corresponding tests Partially implements blueprint v3-api-core-as-extensions Change-Id: Iada86afbfeed055942fef554d12cd36385aa2e1f --- nova/api/openstack/compute/plugins/v3/consoles.py | 144 ++++++++++ .../openstack/compute/plugins/v3/test_consoles.py | 295 +++++++++++++++++++++ setup.cfg | 1 + 3 files changed, 440 insertions(+) create mode 100644 nova/api/openstack/compute/plugins/v3/consoles.py create mode 100644 nova/tests/api/openstack/compute/plugins/v3/test_consoles.py diff --git a/nova/api/openstack/compute/plugins/v3/consoles.py b/nova/api/openstack/compute/plugins/v3/consoles.py new file mode 100644 index 000000000..d99395652 --- /dev/null +++ b/nova/api/openstack/compute/plugins/v3/consoles.py @@ -0,0 +1,144 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010 OpenStack Foundation +# 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. + +import webob +from webob import exc + +from nova.api.openstack import extensions +from nova.api.openstack import wsgi +from nova.api.openstack import xmlutil +from nova.console import api as console_api +from nova import exception + + +def _translate_keys(cons): + """Coerces a console instance into proper dictionary format.""" + pool = cons['pool'] + info = {'id': cons['id'], + 'console_type': pool['console_type']} + return dict(console=info) + + +def _translate_detail_keys(cons): + """Coerces a console instance into proper dictionary format with detail.""" + pool = cons['pool'] + info = {'id': cons['id'], + 'console_type': pool['console_type'], + 'password': cons['password'], + 'instance_name': cons['instance_name'], + 'port': cons['port'], + 'host': pool['public_hostname']} + return dict(console=info) + + +class ConsoleTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('console', selector='console') + + id_elem = xmlutil.SubTemplateElement(root, 'id', selector='id') + id_elem.text = xmlutil.Selector() + + port_elem = xmlutil.SubTemplateElement(root, 'port', selector='port') + port_elem.text = xmlutil.Selector() + + host_elem = xmlutil.SubTemplateElement(root, 'host', selector='host') + host_elem.text = xmlutil.Selector() + + passwd_elem = xmlutil.SubTemplateElement(root, 'password', + selector='password') + passwd_elem.text = xmlutil.Selector() + + constype_elem = xmlutil.SubTemplateElement(root, 'console_type', + selector='console_type') + constype_elem.text = xmlutil.Selector() + + return xmlutil.MasterTemplate(root, 1) + + +class ConsolesTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('consoles') + console = xmlutil.SubTemplateElement(root, 'console', + selector='consoles') + console.append(ConsoleTemplate()) + + return xmlutil.MasterTemplate(root, 1) + + +class ConsolesController(object): + """The Consoles controller for the OpenStack API.""" + + def __init__(self): + self.console_api = console_api.API() + + @wsgi.serializers(xml=ConsolesTemplate) + def index(self, req, server_id): + """Returns a list of consoles for this instance.""" + consoles = self.console_api.get_consoles( + req.environ['nova.context'], + server_id) + return dict(consoles=[_translate_keys(console) + for console in consoles]) + + def create(self, req, server_id): + """Creates a new console.""" + self.console_api.create_console( + req.environ['nova.context'], server_id) + + @wsgi.serializers(xml=ConsoleTemplate) + def show(self, req, server_id, id): + """Shows in-depth information on a specific console.""" + try: + console = self.console_api.get_console( + req.environ['nova.context'], + server_id, + int(id)) + except exception.NotFound: + raise exc.HTTPNotFound() + return _translate_detail_keys(console) + + def delete(self, req, server_id, id): + """Deletes a console.""" + try: + self.console_api.delete_console(req.environ['nova.context'], + server_id, + int(id)) + except exception.NotFound: + raise exc.HTTPNotFound() + return webob.Response(status_int=202) + + +class Consoles(extensions.V3APIExtensionBase): + """Consoles.""" + + name = "Consoles" + alias = "consoles" + namespace = "http://docs.openstack.org/compute/core/consoles/v3" + version = 1 + + def get_resources(self): + parent = {'member_name': 'server', + 'collection_name': 'servers'} + resources = [ + extensions.ResourceExtension( + 'consoles', ConsolesController(), parent=parent, + member_name='console')] + + return resources + + def get_controller_extensions(self): + return [] diff --git a/nova/tests/api/openstack/compute/plugins/v3/test_consoles.py b/nova/tests/api/openstack/compute/plugins/v3/test_consoles.py new file mode 100644 index 000000000..2f84af434 --- /dev/null +++ b/nova/tests/api/openstack/compute/plugins/v3/test_consoles.py @@ -0,0 +1,295 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010-2011 OpenStack Foundation +# Copyright 2011 Piston Cloud Computing, 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. + +import datetime +import uuid as stdlib_uuid + +from lxml import etree +import webob + +from nova.api.openstack.compute.plugins.v3 import consoles +from nova.compute import vm_states +from nova import console +from nova import db +from nova import exception +from nova.openstack.common import timeutils +from nova import test +from nova.tests.api.openstack import fakes +from nova.tests import matchers + + +FAKE_UUID = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa' + + +class FakeInstanceDB(object): + + def __init__(self): + self.instances_by_id = {} + self.ids_by_uuid = {} + self.max_id = 0 + + def return_server_by_id(self, context, id): + if id not in self.instances_by_id: + self._add_server(id=id) + return dict(self.instances_by_id[id]) + + def return_server_by_uuid(self, context, uuid): + if uuid not in self.ids_by_uuid: + self._add_server(uuid=uuid) + return dict(self.instances_by_id[self.ids_by_uuid[uuid]]) + + def _add_server(self, id=None, uuid=None): + if id is None: + id = self.max_id + 1 + if uuid is None: + uuid = str(stdlib_uuid.uuid4()) + instance = stub_instance(id, uuid=uuid) + self.instances_by_id[id] = instance + self.ids_by_uuid[uuid] = id + if id > self.max_id: + self.max_id = id + + +def stub_instance(id, user_id='fake', project_id='fake', host=None, + vm_state=None, task_state=None, + reservation_id="", uuid=FAKE_UUID, image_ref="10", + flavor_id="1", name=None, key_name='', + access_ipv4=None, access_ipv6=None, progress=0): + + if host is not None: + host = str(host) + + if key_name: + key_data = 'FAKE' + else: + key_data = '' + + # ReservationID isn't sent back, hack it in there. + server_name = name or "server%s" % id + if reservation_id != "": + server_name = "reservation_%s" % (reservation_id, ) + + instance = { + "id": int(id), + "created_at": datetime.datetime(2010, 10, 10, 12, 0, 0), + "updated_at": datetime.datetime(2010, 11, 11, 11, 0, 0), + "admin_pass": "", + "user_id": user_id, + "project_id": project_id, + "image_ref": image_ref, + "kernel_id": "", + "ramdisk_id": "", + "launch_index": 0, + "key_name": key_name, + "key_data": key_data, + "vm_state": vm_state or vm_states.BUILDING, + "task_state": task_state, + "memory_mb": 0, + "vcpus": 0, + "root_gb": 0, + "hostname": "", + "host": host, + "instance_type": {}, + "user_data": "", + "reservation_id": reservation_id, + "mac_address": "", + "scheduled_at": timeutils.utcnow(), + "launched_at": timeutils.utcnow(), + "terminated_at": timeutils.utcnow(), + "availability_zone": "", + "display_name": server_name, + "display_description": "", + "locked": False, + "metadata": [], + "access_ip_v4": access_ipv4, + "access_ip_v6": access_ipv6, + "uuid": uuid, + "progress": progress} + + return instance + + +class ConsolesControllerTest(test.TestCase): + def setUp(self): + super(ConsolesControllerTest, self).setUp() + self.flags(verbose=True) + self.instance_db = FakeInstanceDB() + self.stubs.Set(db, 'instance_get', + self.instance_db.return_server_by_id) + self.stubs.Set(db, 'instance_get_by_uuid', + self.instance_db.return_server_by_uuid) + self.uuid = str(stdlib_uuid.uuid4()) + self.url = '/v3/fake/servers/%s/consoles' % self.uuid + self.controller = consoles.ConsolesController() + + def test_create_console(self): + def fake_create_console(cons_self, context, instance_id): + self.assertEqual(instance_id, self.uuid) + return {} + self.stubs.Set(console.api.API, 'create_console', fake_create_console) + + req = fakes.HTTPRequestV3.blank(self.url) + self.controller.create(req, self.uuid) + + def test_show_console(self): + def fake_get_console(cons_self, context, instance_id, console_id): + self.assertEqual(instance_id, self.uuid) + self.assertEqual(console_id, 20) + pool = dict(console_type='fake_type', + public_hostname='fake_hostname') + return dict(id=console_id, password='fake_password', + port='fake_port', pool=pool, instance_name='inst-0001') + + expected = {'console': {'id': 20, + 'port': 'fake_port', + 'host': 'fake_hostname', + 'password': 'fake_password', + 'instance_name': 'inst-0001', + 'console_type': 'fake_type'}} + + self.stubs.Set(console.api.API, 'get_console', fake_get_console) + + req = fakes.HTTPRequestV3.blank(self.url + '/20') + res_dict = self.controller.show(req, self.uuid, '20') + self.assertThat(res_dict, matchers.DictMatches(expected)) + + def test_show_console_unknown_console(self): + def fake_get_console(cons_self, context, instance_id, console_id): + raise exception.ConsoleNotFound(console_id=console_id) + + self.stubs.Set(console.api.API, 'get_console', fake_get_console) + + req = fakes.HTTPRequestV3.blank(self.url + '/20') + self.assertRaises(webob.exc.HTTPNotFound, self.controller.show, + req, self.uuid, '20') + + def test_show_console_unknown_instance(self): + def fake_get_console(cons_self, context, instance_id, console_id): + raise exception.InstanceNotFound(instance_id=instance_id) + + self.stubs.Set(console.api.API, 'get_console', fake_get_console) + + req = fakes.HTTPRequestV3.blank(self.url + '/20') + self.assertRaises(webob.exc.HTTPNotFound, self.controller.show, + req, self.uuid, '20') + + def test_list_consoles(self): + def fake_get_consoles(cons_self, context, instance_id): + self.assertEqual(instance_id, self.uuid) + + pool1 = dict(console_type='fake_type', + public_hostname='fake_hostname') + cons1 = dict(id=10, password='fake_password', + port='fake_port', pool=pool1) + pool2 = dict(console_type='fake_type2', + public_hostname='fake_hostname2') + cons2 = dict(id=11, password='fake_password2', + port='fake_port2', pool=pool2) + return [cons1, cons2] + + expected = {'consoles': + [{'console': {'id': 10, 'console_type': 'fake_type'}}, + {'console': {'id': 11, 'console_type': 'fake_type2'}}]} + + self.stubs.Set(console.api.API, 'get_consoles', fake_get_consoles) + + req = fakes.HTTPRequestV3.blank(self.url) + res_dict = self.controller.index(req, self.uuid) + self.assertThat(res_dict, matchers.DictMatches(expected)) + + def test_delete_console(self): + def fake_get_console(cons_self, context, instance_id, console_id): + self.assertEqual(instance_id, self.uuid) + self.assertEqual(console_id, 20) + pool = dict(console_type='fake_type', + public_hostname='fake_hostname') + return dict(id=console_id, password='fake_password', + port='fake_port', pool=pool) + + def fake_delete_console(cons_self, context, instance_id, console_id): + self.assertEqual(instance_id, self.uuid) + self.assertEqual(console_id, 20) + + self.stubs.Set(console.api.API, 'get_console', fake_get_console) + self.stubs.Set(console.api.API, 'delete_console', fake_delete_console) + + req = fakes.HTTPRequestV3.blank(self.url + '/20') + self.controller.delete(req, self.uuid, '20') + + def test_delete_console_unknown_console(self): + def fake_delete_console(cons_self, context, instance_id, console_id): + raise exception.ConsoleNotFound(console_id=console_id) + + self.stubs.Set(console.api.API, 'delete_console', fake_delete_console) + + req = fakes.HTTPRequestV3.blank(self.url + '/20') + self.assertRaises(webob.exc.HTTPNotFound, self.controller.delete, + req, self.uuid, '20') + + def test_delete_console_unknown_instance(self): + def fake_delete_console(cons_self, context, instance_id, console_id): + raise exception.InstanceNotFound(instance_id=instance_id) + + self.stubs.Set(console.api.API, 'delete_console', fake_delete_console) + + req = fakes.HTTPRequestV3.blank(self.url + '/20') + self.assertRaises(webob.exc.HTTPNotFound, self.controller.delete, + req, self.uuid, '20') + + +class TestConsolesXMLSerializer(test.TestCase): + def test_show(self): + fixture = {'console': {'id': 20, + 'password': 'fake_password', + 'port': 'fake_port', + 'host': 'fake_hostname', + 'console_type': 'fake_type'}} + + output = consoles.ConsoleTemplate().serialize(fixture) + res_tree = etree.XML(output) + + self.assertEqual(res_tree.tag, 'console') + self.assertEqual(res_tree.xpath('id')[0].text, '20') + self.assertEqual(res_tree.xpath('port')[0].text, 'fake_port') + self.assertEqual(res_tree.xpath('host')[0].text, 'fake_hostname') + self.assertEqual(res_tree.xpath('password')[0].text, 'fake_password') + self.assertEqual(res_tree.xpath('console_type')[0].text, 'fake_type') + + def test_index(self): + fixture = {'consoles': [{'console': {'id': 10, + 'console_type': 'fake_type'}}, + {'console': {'id': 11, + 'console_type': 'fake_type2'}}]} + + output = consoles.ConsolesTemplate().serialize(fixture) + res_tree = etree.XML(output) + + self.assertEqual(res_tree.tag, 'consoles') + self.assertEqual(len(res_tree), 2) + self.assertEqual(res_tree[0].tag, 'console') + self.assertEqual(res_tree[1].tag, 'console') + self.assertEqual(len(res_tree[0]), 1) + self.assertEqual(res_tree[0][0].tag, 'console') + self.assertEqual(len(res_tree[1]), 1) + self.assertEqual(res_tree[1][0].tag, 'console') + self.assertEqual(res_tree[0][0].xpath('id')[0].text, '10') + self.assertEqual(res_tree[1][0].xpath('id')[0].text, '11') + self.assertEqual(res_tree[0][0].xpath('console_type')[0].text, + 'fake_type') + self.assertEqual(res_tree[1][0].xpath('console_type')[0].text, + 'fake_type2') diff --git a/setup.cfg b/setup.cfg index bad19611f..8e6e77186 100644 --- a/setup.cfg +++ b/setup.cfg @@ -59,6 +59,7 @@ nova.api.v3.extensions = servers = nova.api.openstack.compute.plugins.v3.servers:Servers keypairs = nova.api.openstack.compute.plugins.v3.keypairs:Keypairs ips = nova.api.openstack.compute.plugins.v3.ips:IPs + consoles = nova.api.openstack.compute.plugins.v3.consoles:Consoles nova.api.v3.extensions.server.create = keypairs_create = nova.api.openstack.compute.plugins.v3.keypairs:Keypairs -- cgit