summaryrefslogtreecommitdiffstats
path: root/nova/api
diff options
context:
space:
mode:
authorBrian Lamar <brian.lamar@rackspace.com>2011-03-30 09:38:09 -0400
committerBrian Lamar <brian.lamar@rackspace.com>2011-03-30 09:38:09 -0400
commitd713fbde58258053a5c55c8d748eb544e55a1adc (patch)
tree8c4ae30098e3b68698649fbf83a600760b576bb3 /nova/api
parent2af6fb2a4d3659e9882a6f6d1c8e71bc8f040aba (diff)
parent56b5bcf86f1bee60a4b727414cca1ac5e714d09a (diff)
Merged trunk.
Diffstat (limited to 'nova/api')
-rw-r--r--nova/api/ec2/cloud.py7
-rw-r--r--nova/api/openstack/__init__.py2
-rw-r--r--nova/api/openstack/common.py6
-rw-r--r--nova/api/openstack/contrib/__init__.py22
-rw-r--r--nova/api/openstack/contrib/volumes.py336
-rw-r--r--nova/api/openstack/extensions.py204
-rw-r--r--nova/api/openstack/servers.py12
7 files changed, 525 insertions, 64 deletions
diff --git a/nova/api/ec2/cloud.py b/nova/api/ec2/cloud.py
index 9e34d3317..7ba8dfbea 100644
--- a/nova/api/ec2/cloud.py
+++ b/nova/api/ec2/cloud.py
@@ -536,6 +536,13 @@ class CloudController(object):
return self.compute_api.get_ajax_console(context,
instance_id=instance_id)
+ def get_vnc_console(self, context, instance_id, **kwargs):
+ """Returns vnc browser url. Used by OS dashboard."""
+ ec2_id = instance_id
+ instance_id = ec2utils.ec2_id_to_id(ec2_id)
+ return self.compute_api.get_vnc_console(context,
+ instance_id=instance_id)
+
def describe_volumes(self, context, volume_id=None, **kwargs):
if volume_id:
volumes = []
diff --git a/nova/api/openstack/__init__.py b/nova/api/openstack/__init__.py
index 4f9405075..7545eb0c9 100644
--- a/nova/api/openstack/__init__.py
+++ b/nova/api/openstack/__init__.py
@@ -114,6 +114,8 @@ class APIRouter(wsgi.Router):
_limits = limits.LimitsController()
mapper.resource("limit", "limits", controller=_limits)
+ super(APIRouter, self).__init__(mapper)
+
class APIRouterV10(APIRouter):
"""Define routes specific to OpenStack API V1.0."""
diff --git a/nova/api/openstack/common.py b/nova/api/openstack/common.py
index 8cad1273a..75aeb0a5f 100644
--- a/nova/api/openstack/common.py
+++ b/nova/api/openstack/common.py
@@ -21,6 +21,11 @@ import webob
from nova import exception
from nova import flags
+from nova import log as logging
+
+
+LOG = logging.getLogger('common')
+
FLAGS = flags.FLAGS
@@ -121,4 +126,5 @@ def get_id_from_href(href):
try:
return int(urlparse(href).path.split('/')[-1])
except:
+ LOG.debug(_("Error extracting id from href: %s") % href)
raise webob.exc.HTTPBadRequest(_('could not parse id from href'))
diff --git a/nova/api/openstack/contrib/__init__.py b/nova/api/openstack/contrib/__init__.py
new file mode 100644
index 000000000..b42a1d89d
--- /dev/null
+++ b/nova/api/openstack/contrib/__init__.py
@@ -0,0 +1,22 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2011 Justin Santa Barbara
+# 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
+
+"""Contrib contains extensions that are shipped with nova.
+
+It can't be called 'extensions' because that causes namespacing problems.
+
+"""
diff --git a/nova/api/openstack/contrib/volumes.py b/nova/api/openstack/contrib/volumes.py
new file mode 100644
index 000000000..6efacce52
--- /dev/null
+++ b/nova/api/openstack/contrib/volumes.py
@@ -0,0 +1,336 @@
+# Copyright 2011 Justin Santa Barbara
+# 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 volumes extension."""
+
+from webob import exc
+
+from nova import compute
+from nova import exception
+from nova import flags
+from nova import log as logging
+from nova import volume
+from nova import wsgi
+from nova.api.openstack import common
+from nova.api.openstack import extensions
+from nova.api.openstack import faults
+
+
+LOG = logging.getLogger("nova.api.volumes")
+
+
+FLAGS = flags.FLAGS
+
+
+def _translate_volume_detail_view(context, vol):
+ """Maps keys for volumes details view."""
+
+ d = _translate_volume_summary_view(context, vol)
+
+ # No additional data / lookups at the moment
+
+ return d
+
+
+def _translate_volume_summary_view(context, vol):
+ """Maps keys for volumes summary view."""
+ d = {}
+
+ d['id'] = vol['id']
+ d['status'] = vol['status']
+ d['size'] = vol['size']
+ d['availabilityZone'] = vol['availability_zone']
+ d['createdAt'] = vol['created_at']
+
+ if vol['attach_status'] == 'attached':
+ d['attachments'] = [_translate_attachment_detail_view(context, vol)]
+ else:
+ d['attachments'] = [{}]
+
+ d['displayName'] = vol['display_name']
+ d['displayDescription'] = vol['display_description']
+ return d
+
+
+class VolumeController(wsgi.Controller):
+ """The Volumes API controller for the OpenStack API."""
+
+ _serialization_metadata = {
+ 'application/xml': {
+ "attributes": {
+ "volume": [
+ "id",
+ "status",
+ "size",
+ "availabilityZone",
+ "createdAt",
+ "displayName",
+ "displayDescription",
+ ]}}}
+
+ def __init__(self):
+ self.volume_api = volume.API()
+ super(VolumeController, self).__init__()
+
+ def show(self, req, id):
+ """Return data about the given volume."""
+ context = req.environ['nova.context']
+
+ try:
+ vol = self.volume_api.get(context, id)
+ except exception.NotFound:
+ return faults.Fault(exc.HTTPNotFound())
+
+ return {'volume': _translate_volume_detail_view(context, vol)}
+
+ def delete(self, req, id):
+ """Delete a volume."""
+ context = req.environ['nova.context']
+
+ LOG.audit(_("Delete volume with id: %s"), id, context=context)
+
+ try:
+ self.volume_api.delete(context, volume_id=id)
+ except exception.NotFound:
+ return faults.Fault(exc.HTTPNotFound())
+ return exc.HTTPAccepted()
+
+ def index(self, req):
+ """Returns a summary list of volumes."""
+ return self._items(req, entity_maker=_translate_volume_summary_view)
+
+ def detail(self, req):
+ """Returns a detailed list of volumes."""
+ return self._items(req, entity_maker=_translate_volume_detail_view)
+
+ def _items(self, req, entity_maker):
+ """Returns a list of volumes, transformed through entity_maker."""
+ context = req.environ['nova.context']
+
+ volumes = self.volume_api.get_all(context)
+ limited_list = common.limited(volumes, req)
+ res = [entity_maker(context, vol) for vol in limited_list]
+ return {'volumes': res}
+
+ def create(self, req):
+ """Creates a new volume."""
+ context = req.environ['nova.context']
+
+ env = self._deserialize(req.body, req.get_content_type())
+ if not env:
+ return faults.Fault(exc.HTTPUnprocessableEntity())
+
+ vol = env['volume']
+ size = vol['size']
+ LOG.audit(_("Create volume of %s GB"), size, context=context)
+ new_volume = self.volume_api.create(context, size,
+ vol.get('display_name'),
+ vol.get('display_description'))
+
+ # Work around problem that instance is lazy-loaded...
+ new_volume['instance'] = None
+
+ retval = _translate_volume_detail_view(context, new_volume)
+
+ return {'volume': retval}
+
+
+def _translate_attachment_detail_view(_context, vol):
+ """Maps keys for attachment details view."""
+
+ d = _translate_attachment_summary_view(_context, vol)
+
+ # No additional data / lookups at the moment
+
+ return d
+
+
+def _translate_attachment_summary_view(_context, vol):
+ """Maps keys for attachment summary view."""
+ d = {}
+
+ volume_id = vol['id']
+
+ # NOTE(justinsb): We use the volume id as the id of the attachment object
+ d['id'] = volume_id
+
+ d['volumeId'] = volume_id
+ if vol.get('instance_id'):
+ d['serverId'] = vol['instance_id']
+ if vol.get('mountpoint'):
+ d['device'] = vol['mountpoint']
+
+ return d
+
+
+class VolumeAttachmentController(wsgi.Controller):
+ """The volume attachment API controller for the Openstack API.
+
+ A child resource of the server. Note that we use the volume id
+ as the ID of the attachment (though this is not guaranteed externally)
+
+ """
+
+ _serialization_metadata = {
+ 'application/xml': {
+ 'attributes': {
+ 'volumeAttachment': ['id',
+ 'serverId',
+ 'volumeId',
+ 'device']}}}
+
+ def __init__(self):
+ self.compute_api = compute.API()
+ self.volume_api = volume.API()
+ super(VolumeAttachmentController, self).__init__()
+
+ def index(self, req, server_id):
+ """Returns the list of volume attachments for a given instance."""
+ return self._items(req, server_id,
+ entity_maker=_translate_attachment_summary_view)
+
+ def show(self, req, server_id, id):
+ """Return data about the given volume attachment."""
+ context = req.environ['nova.context']
+
+ volume_id = id
+ try:
+ vol = self.volume_api.get(context, volume_id)
+ except exception.NotFound:
+ LOG.debug("volume_id not found")
+ return faults.Fault(exc.HTTPNotFound())
+
+ if str(vol['instance_id']) != server_id:
+ LOG.debug("instance_id != server_id")
+ return faults.Fault(exc.HTTPNotFound())
+
+ return {'volumeAttachment': _translate_attachment_detail_view(context,
+ vol)}
+
+ def create(self, req, server_id):
+ """Attach a volume to an instance."""
+ context = req.environ['nova.context']
+
+ env = self._deserialize(req.body, req.get_content_type())
+ if not env:
+ return faults.Fault(exc.HTTPUnprocessableEntity())
+
+ instance_id = server_id
+ volume_id = env['volumeAttachment']['volumeId']
+ device = env['volumeAttachment']['device']
+
+ msg = _("Attach volume %(volume_id)s to instance %(server_id)s"
+ " at %(device)s") % locals()
+ LOG.audit(msg, context=context)
+
+ try:
+ self.compute_api.attach_volume(context,
+ instance_id=instance_id,
+ volume_id=volume_id,
+ device=device)
+ except exception.NotFound:
+ return faults.Fault(exc.HTTPNotFound())
+
+ # The attach is async
+ attachment = {}
+ attachment['id'] = volume_id
+ attachment['volumeId'] = volume_id
+
+ # NOTE(justinsb): And now, we have a problem...
+ # The attach is async, so there's a window in which we don't see
+ # the attachment (until the attachment completes). We could also
+ # get problems with concurrent requests. I think we need an
+ # attachment state, and to write to the DB here, but that's a bigger
+ # change.
+ # For now, we'll probably have to rely on libraries being smart
+
+ # TODO(justinsb): How do I return "accepted" here?
+ return {'volumeAttachment': attachment}
+
+ def update(self, _req, _server_id, _id):
+ """Update a volume attachment. We don't currently support this."""
+ return faults.Fault(exc.HTTPBadRequest())
+
+ def delete(self, req, server_id, id):
+ """Detach a volume from an instance."""
+ context = req.environ['nova.context']
+
+ volume_id = id
+ LOG.audit(_("Detach volume %s"), volume_id, context=context)
+
+ try:
+ vol = self.volume_api.get(context, volume_id)
+ except exception.NotFound:
+ return faults.Fault(exc.HTTPNotFound())
+
+ if str(vol['instance_id']) != server_id:
+ LOG.debug("instance_id != server_id")
+ return faults.Fault(exc.HTTPNotFound())
+
+ self.compute_api.detach_volume(context,
+ volume_id=volume_id)
+
+ return exc.HTTPAccepted()
+
+ def _items(self, req, server_id, entity_maker):
+ """Returns a list of attachments, transformed through entity_maker."""
+ context = req.environ['nova.context']
+
+ try:
+ instance = self.compute_api.get(context, server_id)
+ except exception.NotFound:
+ return faults.Fault(exc.HTTPNotFound())
+
+ volumes = instance['volumes']
+ limited_list = common.limited(volumes, req)
+ res = [entity_maker(context, vol) for vol in limited_list]
+ return {'volumeAttachments': res}
+
+
+class Volumes(extensions.ExtensionDescriptor):
+ def get_name(self):
+ return "Volumes"
+
+ def get_alias(self):
+ return "VOLUMES"
+
+ def get_description(self):
+ return "Volumes support"
+
+ def get_namespace(self):
+ return "http://docs.openstack.org/ext/volumes/api/v1.1"
+
+ def get_updated(self):
+ return "2011-03-25T00:00:00+00:00"
+
+ def get_resources(self):
+ resources = []
+
+ # NOTE(justinsb): No way to provide singular name ('volume')
+ # Does this matter?
+ res = extensions.ResourceExtension('volumes',
+ VolumeController(),
+ collection_actions={'detail': 'GET'}
+ )
+ resources.append(res)
+
+ res = extensions.ResourceExtension('volume_attachments',
+ VolumeAttachmentController(),
+ parent=dict(
+ member_name='server',
+ collection_name='servers'))
+ resources.append(res)
+
+ return resources
diff --git a/nova/api/openstack/extensions.py b/nova/api/openstack/extensions.py
index b9b7f998d..fb1dccb28 100644
--- a/nova/api/openstack/extensions.py
+++ b/nova/api/openstack/extensions.py
@@ -1,6 +1,7 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2011 OpenStack LLC.
+# Copyright 2011 Justin Santa Barbara
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
@@ -16,12 +17,14 @@
# under the License.
import imp
+import inspect
import os
import sys
import routes
import webob.dec
import webob.exc
+from nova import exception
from nova import flags
from nova import log as logging
from nova import wsgi
@@ -34,6 +37,84 @@ LOG = logging.getLogger('extensions')
FLAGS = flags.FLAGS
+class ExtensionDescriptor(object):
+ """Base class that defines the contract for extensions.
+
+ Note that you don't have to derive from this class to have a valid
+ extension; it is purely a convenience.
+
+ """
+
+ def get_name(self):
+ """The name of the extension.
+
+ e.g. 'Fox In Socks'
+
+ """
+ raise NotImplementedError()
+
+ def get_alias(self):
+ """The alias for the extension.
+
+ e.g. 'FOXNSOX'
+
+ """
+ raise NotImplementedError()
+
+ def get_description(self):
+ """Friendly description for the extension.
+
+ e.g. 'The Fox In Socks Extension'
+
+ """
+ raise NotImplementedError()
+
+ def get_namespace(self):
+ """The XML namespace for the extension.
+
+ e.g. 'http://www.fox.in.socks/api/ext/pie/v1.0'
+
+ """
+ raise NotImplementedError()
+
+ def get_updated(self):
+ """The timestamp when the extension was last updated.
+
+ e.g. '2011-01-22T13:25:27-06:00'
+
+ """
+ # NOTE(justinsb): Not sure of the purpose of this is, vs the XML NS
+ raise NotImplementedError()
+
+ def get_resources(self):
+ """List of extensions.ResourceExtension extension objects.
+
+ Resources define new nouns, and are accessible through URLs.
+
+ """
+ resources = []
+ return resources
+
+ def get_actions(self):
+ """List of extensions.ActionExtension extension objects.
+
+ Actions are verbs callable from the API.
+
+ """
+ actions = []
+ return actions
+
+ def get_response_extensions(self):
+ """List of extensions.ResponseExtension extension objects.
+
+ Response extensions are used to insert information into existing
+ response data.
+
+ """
+ response_exts = []
+ return response_exts
+
+
class ActionExtensionController(wsgi.Controller):
def __init__(self, application):
@@ -94,45 +175,38 @@ class ExtensionController(wsgi.Controller):
ext_data['description'] = ext.get_description()
ext_data['namespace'] = ext.get_namespace()
ext_data['updated'] = ext.get_updated()
- ext_data['links'] = [] # TODO: implement extension links
+ ext_data['links'] = [] # TODO(dprince): implement extension links
return ext_data
def index(self, req):
extensions = []
- for alias, ext in self.extension_manager.extensions.iteritems():
+ for _alias, ext in self.extension_manager.extensions.iteritems():
extensions.append(self._translate(ext))
return dict(extensions=extensions)
def show(self, req, id):
- # NOTE: the extensions alias is used as the 'id' for show
+ # NOTE(dprince): the extensions alias is used as the 'id' for show
ext = self.extension_manager.extensions[id]
return self._translate(ext)
def delete(self, req, id):
- raise faults.Fault(exc.HTTPNotFound())
+ raise faults.Fault(webob.exc.HTTPNotFound())
def create(self, req):
- raise faults.Fault(exc.HTTPNotFound())
-
- def delete(self, req, id):
- raise faults.Fault(exc.HTTPNotFound())
+ raise faults.Fault(webob.exc.HTTPNotFound())
class ExtensionMiddleware(wsgi.Middleware):
- """
- Extensions middleware that intercepts configured routes for extensions.
- """
+ """Extensions middleware for WSGI."""
@classmethod
def factory(cls, global_config, **local_config):
- """ paste factory """
+ """Paste factory."""
def _factory(app):
return cls(app, **local_config)
return _factory
def _action_ext_controllers(self, application, ext_mgr, mapper):
- """
- Return a dict of ActionExtensionController objects by collection
- """
+ """Return a dict of ActionExtensionController-s by collection."""
action_controllers = {}
for action in ext_mgr.get_actions():
if not action.collection in action_controllers.keys():
@@ -151,9 +225,7 @@ class ExtensionMiddleware(wsgi.Middleware):
return action_controllers
def _response_ext_controllers(self, application, ext_mgr, mapper):
- """
- Return a dict of ResponseExtensionController objects by collection
- """
+ """Returns a dict of ResponseExtensionController-s by collection."""
response_ext_controllers = {}
for resp_ext in ext_mgr.get_response_extensions():
if not resp_ext.key in response_ext_controllers.keys():
@@ -212,18 +284,18 @@ class ExtensionMiddleware(wsgi.Middleware):
@webob.dec.wsgify(RequestClass=wsgi.Request)
def __call__(self, req):
- """
- Route the incoming request with router.
- """
+ """Route the incoming request with router."""
req.environ['extended.app'] = self.application
return self._router
@staticmethod
@webob.dec.wsgify(RequestClass=wsgi.Request)
def _dispatch(req):
- """
+ """Dispatch the request.
+
Returns the routed WSGI app's response or defers to the extended
application.
+
"""
match = req.environ['wsgiorg.routing_args'][1]
if not match:
@@ -233,10 +305,11 @@ class ExtensionMiddleware(wsgi.Middleware):
class ExtensionManager(object):
- """
- Load extensions from the configured extension path.
- See nova/tests/api/openstack/extensions/foxinsocks.py for an example
- extension implementation.
+ """Load extensions from the configured extension path.
+
+ See nova/tests/api/openstack/extensions/foxinsocks/extension.py for an
+ example extension implementation.
+
"""
def __init__(self, path):
@@ -244,12 +317,10 @@ class ExtensionManager(object):
self.path = path
self.extensions = {}
- self._load_extensions()
+ self._load_all_extensions()
def get_resources(self):
- """
- returns a list of ResourceExtension objects
- """
+ """Returns a list of ResourceExtension objects."""
resources = []
resources.append(ResourceExtension('extensions',
ExtensionController(self)))
@@ -257,40 +328,37 @@ class ExtensionManager(object):
try:
resources.extend(ext.get_resources())
except AttributeError:
- # NOTE: Extension aren't required to have resource extensions
+ # NOTE(dprince): Extension aren't required to have resource
+ # extensions
pass
return resources
def get_actions(self):
- """
- returns a list of ActionExtension objects
- """
+ """Returns a list of ActionExtension objects."""
actions = []
for alias, ext in self.extensions.iteritems():
try:
actions.extend(ext.get_actions())
except AttributeError:
- # NOTE: Extension aren't required to have action extensions
+ # NOTE(dprince): Extension aren't required to have action
+ # extensions
pass
return actions
def get_response_extensions(self):
- """
- returns a list of ResponseExtension objects
- """
+ """Returns a list of ResponseExtension objects."""
response_exts = []
for alias, ext in self.extensions.iteritems():
try:
response_exts.extend(ext.get_response_extensions())
except AttributeError:
- # NOTE: Extension aren't required to have response extensions
+ # NOTE(dprince): Extension aren't required to have response
+ # extensions
pass
return response_exts
def _check_extension(self, extension):
- """
- Checks for required methods in extension objects.
- """
+ """Checks for required methods in extension objects."""
try:
LOG.debug(_('Ext name: %s'), extension.get_name())
LOG.debug(_('Ext alias: %s'), extension.get_alias())
@@ -300,43 +368,59 @@ class ExtensionManager(object):
except AttributeError as ex:
LOG.exception(_("Exception loading extension: %s"), unicode(ex))
- def _load_extensions(self):
- """
+ def _load_all_extensions(self):
+ """Load extensions from the configured path.
+
Load extensions from the configured path. The extension name is
constructed from the module_name. If your extension module was named
widgets.py the extension class within that module should be
'Widgets'.
+ In addition, extensions are loaded from the 'contrib' directory.
+
See nova/tests/api/openstack/extensions/foxinsocks.py for an example
extension implementation.
+
"""
- if not os.path.exists(self.path):
- return
+ if os.path.exists(self.path):
+ self._load_all_extensions_from_path(self.path)
- for f in os.listdir(self.path):
+ contrib_path = os.path.join(os.path.dirname(__file__), "contrib")
+ if os.path.exists(contrib_path):
+ self._load_all_extensions_from_path(contrib_path)
+
+ def _load_all_extensions_from_path(self, path):
+ for f in os.listdir(path):
LOG.audit(_('Loading extension file: %s'), f)
mod_name, file_ext = os.path.splitext(os.path.split(f)[-1])
- ext_path = os.path.join(self.path, f)
+ ext_path = os.path.join(path, f)
if file_ext.lower() == '.py' and not mod_name.startswith('_'):
mod = imp.load_source(mod_name, ext_path)
ext_name = mod_name[0].upper() + mod_name[1:]
new_ext_class = getattr(mod, ext_name, None)
if not new_ext_class:
LOG.warn(_('Did not find expected name '
- '"%(ext_name)" in %(file)s'),
+ '"%(ext_name)s" in %(file)s'),
{'ext_name': ext_name,
- 'file': ext_path})
+ 'file': ext_path})
continue
new_ext = new_ext_class()
self._check_extension(new_ext)
- self.extensions[new_ext.get_alias()] = new_ext
+ self._add_extension(new_ext)
+
+ def _add_extension(self, ext):
+ alias = ext.get_alias()
+ LOG.audit(_('Loaded extension: %s'), alias)
+
+ self._check_extension(ext)
+
+ if alias in self.extensions:
+ raise exception.Error("Found duplicate extension: %s" % alias)
+ self.extensions[alias] = ext
class ResponseExtension(object):
- """
- ResponseExtension objects can be used to add data to responses from
- core nova OpenStack API controllers.
- """
+ """Add data to responses from core nova OpenStack API controllers."""
def __init__(self, method, url_route, handler):
self.url_route = url_route
@@ -346,10 +430,7 @@ class ResponseExtension(object):
class ActionExtension(object):
- """
- ActionExtension objects can be used to add custom actions to core nova
- nova OpenStack API controllers.
- """
+ """Add custom actions to core nova OpenStack API controllers."""
def __init__(self, collection, action_name, handler):
self.collection = collection
@@ -358,10 +439,7 @@ class ActionExtension(object):
class ResourceExtension(object):
- """
- ResourceExtension objects can be used to add top level resources
- to the OpenStack API in nova.
- """
+ """Add top level resources to the OpenStack API in nova."""
def __init__(self, collection, controller, parent=None,
collection_actions={}, member_actions={}):
diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py
index 75a305a14..6bd173bb8 100644
--- a/nova/api/openstack/servers.py
+++ b/nova/api/openstack/servers.py
@@ -477,7 +477,7 @@ class Controller(wsgi.Controller):
@scheduler_api.redirect_handler
def get_ajax_console(self, req, id):
- """ Returns a url to an instance's ajaxterm console. """
+ """Returns a url to an instance's ajaxterm console."""
try:
self.compute_api.get_ajax_console(req.environ['nova.context'],
int(id))
@@ -486,6 +486,16 @@ class Controller(wsgi.Controller):
return exc.HTTPAccepted()
@scheduler_api.redirect_handler
+ def get_vnc_console(self, req, id):
+ """Returns a url to an instance's ajaxterm console."""
+ try:
+ self.compute_api.get_vnc_console(req.environ['nova.context'],
+ int(id))
+ except exception.NotFound:
+ return faults.Fault(exc.HTTPNotFound())
+ return exc.HTTPAccepted()
+
+ @scheduler_api.redirect_handler
def diagnostics(self, req, id):
"""Permit Admins to retrieve server diagnostics."""
ctxt = req.environ["nova.context"]