summaryrefslogtreecommitdiffstats
path: root/nova/api/openstack/compute/plugins
diff options
context:
space:
mode:
Diffstat (limited to 'nova/api/openstack/compute/plugins')
-rw-r--r--nova/api/openstack/compute/plugins/v3/cells.py347
-rw-r--r--nova/api/openstack/compute/plugins/v3/flavor_access.py220
-rw-r--r--nova/api/openstack/compute/plugins/v3/flavor_disabled.py91
-rw-r--r--nova/api/openstack/compute/plugins/v3/flavors.py169
-rw-r--r--nova/api/openstack/compute/plugins/v3/images.py242
-rw-r--r--nova/api/openstack/compute/plugins/v3/quota_classes.py103
-rw-r--r--nova/api/openstack/compute/plugins/v3/server_diagnostics.py71
7 files changed, 1243 insertions, 0 deletions
diff --git a/nova/api/openstack/compute/plugins/v3/cells.py b/nova/api/openstack/compute/plugins/v3/cells.py
new file mode 100644
index 000000000..e07792018
--- /dev/null
+++ b/nova/api/openstack/compute/plugins/v3/cells.py
@@ -0,0 +1,347 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2011-2012 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.
+
+"""The cells extension."""
+
+from oslo.config import cfg
+from webob import exc
+
+from nova.api.openstack import common
+from nova.api.openstack import extensions
+from nova.api.openstack import wsgi
+from nova.api.openstack import xmlutil
+from nova.cells import rpcapi as cells_rpcapi
+from nova.compute import api as compute
+from nova import db
+from nova import exception
+from nova.openstack.common import log as logging
+from nova.openstack.common import timeutils
+
+
+LOG = logging.getLogger(__name__)
+CONF = cfg.CONF
+CONF.import_opt('name', 'nova.cells.opts', group='cells')
+CONF.import_opt('capabilities', 'nova.cells.opts', group='cells')
+
+ALIAS = "os-cells"
+authorize = extensions.extension_authorizer('compute', 'v3:' + ALIAS)
+
+
+def make_cell(elem):
+ elem.set('name')
+ elem.set('username')
+ elem.set('type')
+ elem.set('rpc_host')
+ elem.set('rpc_port')
+
+ caps = xmlutil.SubTemplateElement(elem, 'capabilities',
+ selector='capabilities')
+ cap = xmlutil.SubTemplateElement(caps, xmlutil.Selector(0),
+ selector=xmlutil.get_items)
+ cap.text = 1
+ make_capacity(elem)
+
+
+def make_capacity(cell):
+
+ def get_units_by_mb(capacity_info):
+ return capacity_info['units_by_mb'].items()
+
+ capacity = xmlutil.SubTemplateElement(cell, 'capacities',
+ selector='capacities')
+
+ ram_free = xmlutil.SubTemplateElement(capacity, 'ram_free',
+ selector='ram_free')
+ ram_free.set('total_mb', 'total_mb')
+ unit_by_mb = xmlutil.SubTemplateElement(ram_free, 'unit_by_mb',
+ selector=get_units_by_mb)
+ unit_by_mb.set('mb', 0)
+ unit_by_mb.set('unit', 1)
+
+ disk_free = xmlutil.SubTemplateElement(capacity, 'disk_free',
+ selector='disk_free')
+ disk_free.set('total_mb', 'total_mb')
+ unit_by_mb = xmlutil.SubTemplateElement(disk_free, 'unit_by_mb',
+ selector=get_units_by_mb)
+ unit_by_mb.set('mb', 0)
+ unit_by_mb.set('unit', 1)
+
+cell_nsmap = {None: wsgi.XMLNS_V10}
+
+
+class CellTemplate(xmlutil.TemplateBuilder):
+ def construct(self):
+ root = xmlutil.TemplateElement('cell', selector='cell')
+ make_cell(root)
+ return xmlutil.MasterTemplate(root, 1, nsmap=cell_nsmap)
+
+
+class CellsTemplate(xmlutil.TemplateBuilder):
+ def construct(self):
+ root = xmlutil.TemplateElement('cells')
+ elem = xmlutil.SubTemplateElement(root, 'cell', selector='cells')
+ make_cell(elem)
+ return xmlutil.MasterTemplate(root, 1, nsmap=cell_nsmap)
+
+
+class CellDeserializer(wsgi.XMLDeserializer):
+ """Deserializer to handle xml-formatted cell create requests."""
+
+ def _extract_capabilities(self, cap_node):
+ caps = {}
+ for cap in cap_node.childNodes:
+ cap_name = cap.tagName
+ caps[cap_name] = self.extract_text(cap)
+ return caps
+
+ def _extract_cell(self, node):
+ cell = {}
+ cell_node = self.find_first_child_named(node, 'cell')
+
+ extract_fns = {'capabilities': self._extract_capabilities}
+
+ for child in cell_node.childNodes:
+ name = child.tagName
+ extract_fn = extract_fns.get(name, self.extract_text)
+ cell[name] = extract_fn(child)
+ return cell
+
+ def default(self, string):
+ """Deserialize an xml-formatted cell create request."""
+ node = xmlutil.safe_minidom_parse_string(string)
+
+ return {'body': {'cell': self._extract_cell(node)}}
+
+
+def _filter_keys(item, keys):
+ """
+ Filters all model attributes except for keys
+ item is a dict
+
+ """
+ return dict((k, v) for k, v in item.iteritems() if k in keys)
+
+
+def _scrub_cell(cell, detail=False):
+ keys = ['name', 'username', 'rpc_host', 'rpc_port']
+ if detail:
+ keys.append('capabilities')
+
+ cell_info = _filter_keys(cell, keys)
+ cell_info['type'] = 'parent' if cell['is_parent'] else 'child'
+ return cell_info
+
+
+class CellsController(object):
+ """Controller for Cell resources."""
+
+ def __init__(self):
+ self.compute_api = compute.API()
+ self.cells_rpcapi = cells_rpcapi.CellsAPI()
+
+ def _get_cells(self, ctxt, req, detail=False):
+ """Return all cells."""
+ # Ask the CellsManager for the most recent data
+ items = self.cells_rpcapi.get_cell_info_for_neighbors(ctxt)
+ items = common.limited(items, req)
+ items = [_scrub_cell(item, detail=detail) for item in items]
+ return dict(cells=items)
+
+ @wsgi.serializers(xml=CellsTemplate)
+ def index(self, req):
+ """Return all cells in brief."""
+ ctxt = req.environ['nova.context']
+ authorize(ctxt)
+ return self._get_cells(ctxt, req)
+
+ @wsgi.serializers(xml=CellsTemplate)
+ def detail(self, req):
+ """Return all cells in detail."""
+ ctxt = req.environ['nova.context']
+ authorize(ctxt)
+ return self._get_cells(ctxt, req, detail=True)
+
+ @wsgi.serializers(xml=CellTemplate)
+ def info(self, req):
+ """Return name and capabilities for this cell."""
+ context = req.environ['nova.context']
+ authorize(context)
+ cell_capabs = {}
+ my_caps = CONF.cells.capabilities
+ for cap in my_caps:
+ key, value = cap.split('=')
+ cell_capabs[key] = value
+ cell = {'name': CONF.cells.name,
+ 'type': 'self',
+ 'rpc_host': None,
+ 'rpc_port': 0,
+ 'username': None,
+ 'capabilities': cell_capabs}
+ return dict(cell=cell)
+
+ @wsgi.serializers(xml=CellTemplate)
+ def capacities(self, req, id=None):
+ """Return capacities for a given cell or all cells."""
+ # TODO(kaushikc): return capacities as a part of cell info and
+ # cells detail calls in v3, along with capabilities
+ context = req.environ['nova.context']
+ authorize(context)
+ try:
+ capacities = self.cells_rpcapi.get_capacities(context,
+ cell_name=id)
+ except exception.CellNotFound:
+ msg = (_("Cell %(id)s not found.") % {'id': id})
+ raise exc.HTTPNotFound(explanation=msg)
+
+ return dict(cell={"capacities": capacities})
+
+ @wsgi.serializers(xml=CellTemplate)
+ def show(self, req, id):
+ """Return data about the given cell name. 'id' is a cell name."""
+ context = req.environ['nova.context']
+ authorize(context)
+ try:
+ cell = db.cell_get(context, id)
+ except exception.CellNotFound:
+ raise exc.HTTPNotFound()
+ return dict(cell=_scrub_cell(cell))
+
+ def delete(self, req, id):
+ """Delete a child or parent cell entry. 'id' is a cell name."""
+ context = req.environ['nova.context']
+ authorize(context)
+ num_deleted = db.cell_delete(context, id)
+ if num_deleted == 0:
+ raise exc.HTTPNotFound()
+ return {}
+
+ def _validate_cell_name(self, cell_name):
+ """Validate cell name is not empty and doesn't contain '!' or '.'."""
+ if not cell_name:
+ msg = _("Cell name cannot be empty")
+ LOG.error(msg)
+ raise exc.HTTPBadRequest(explanation=msg)
+ if '!' in cell_name or '.' in cell_name:
+ msg = _("Cell name cannot contain '!' or '.'")
+ LOG.error(msg)
+ raise exc.HTTPBadRequest(explanation=msg)
+
+ def _validate_cell_type(self, cell_type):
+ """Validate cell_type is 'parent' or 'child'."""
+ if cell_type not in ['parent', 'child']:
+ msg = _("Cell type must be 'parent' or 'child'")
+ LOG.error(msg)
+ raise exc.HTTPBadRequest(explanation=msg)
+
+ def _convert_cell_type(self, cell):
+ """Convert cell['type'] to is_parent boolean."""
+ if 'type' in cell:
+ self._validate_cell_type(cell['type'])
+ cell['is_parent'] = cell['type'] == 'parent'
+ del cell['type']
+ else:
+ cell['is_parent'] = False
+
+ @wsgi.serializers(xml=CellTemplate)
+ @wsgi.deserializers(xml=CellDeserializer)
+ def create(self, req, body):
+ """Create a child cell entry."""
+ context = req.environ['nova.context']
+ authorize(context)
+ if 'cell' not in body:
+ msg = _("No cell information in request")
+ LOG.error(msg)
+ raise exc.HTTPBadRequest(explanation=msg)
+ cell = body['cell']
+ if 'name' not in cell:
+ msg = _("No cell name in request")
+ LOG.error(msg)
+ raise exc.HTTPBadRequest(explanation=msg)
+ self._validate_cell_name(cell['name'])
+ self._convert_cell_type(cell)
+ cell = db.cell_create(context, cell)
+ return dict(cell=_scrub_cell(cell))
+
+ @wsgi.serializers(xml=CellTemplate)
+ @wsgi.deserializers(xml=CellDeserializer)
+ def update(self, req, id, body):
+ """Update a child cell entry. 'id' is the cell name to update."""
+ context = req.environ['nova.context']
+ authorize(context)
+ if 'cell' not in body:
+ msg = _("No cell information in request")
+ LOG.error(msg)
+ raise exc.HTTPBadRequest(explanation=msg)
+ cell = body['cell']
+ cell.pop('id', None)
+ if 'name' in cell:
+ self._validate_cell_name(cell['name'])
+ self._convert_cell_type(cell)
+ try:
+ cell = db.cell_update(context, id, cell)
+ except exception.CellNotFound:
+ raise exc.HTTPNotFound()
+ return dict(cell=_scrub_cell(cell))
+
+ def sync_instances(self, req, body):
+ """Tell all cells to sync instance info."""
+ context = req.environ['nova.context']
+ authorize(context)
+ project_id = body.pop('project_id', None)
+ deleted = body.pop('deleted', False)
+ updated_since = body.pop('updated_since', None)
+ if body:
+ msg = _("Only 'updated_since' and 'project_id' are understood.")
+ raise exc.HTTPBadRequest(explanation=msg)
+ if updated_since:
+ try:
+ timeutils.parse_isotime(updated_since)
+ except ValueError:
+ msg = _('Invalid changes-since value')
+ raise exc.HTTPBadRequest(explanation=msg)
+ self.cells_rpcapi.sync_instances(context, project_id=project_id,
+ updated_since=updated_since, deleted=deleted)
+
+
+class Cells(extensions.V3APIExtensionBase):
+ """Enables cells-related functionality such as adding neighbor cells,
+ listing neighbor cells, and getting the capabilities of the local cell.
+ """
+
+ name = "Cells"
+ alias = ALIAS
+ namespace = "http://docs.openstack.org/compute/ext/cells/api/v3"
+ version = 1
+
+ def get_resources(self):
+ coll_actions = {
+ 'detail': 'GET',
+ 'info': 'GET',
+ 'sync_instances': 'POST',
+ 'capacities': 'GET',
+ }
+ memb_actions = {
+ 'capacities': 'GET',
+ }
+
+ res = extensions.ResourceExtension(ALIAS, CellsController(),
+ collection_actions=coll_actions,
+ member_actions=memb_actions)
+ return [res]
+
+ def get_controller_extensions(self):
+ return []
diff --git a/nova/api/openstack/compute/plugins/v3/flavor_access.py b/nova/api/openstack/compute/plugins/v3/flavor_access.py
new file mode 100644
index 000000000..459a4041b
--- /dev/null
+++ b/nova/api/openstack/compute/plugins/v3/flavor_access.py
@@ -0,0 +1,220 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright (c) 2011 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.
+
+"""The flavor access extension."""
+
+import webob
+
+from nova.api.openstack import extensions
+from nova.api.openstack import wsgi
+from nova.api.openstack import xmlutil
+from nova.compute import flavors
+from nova import exception
+
+ALIAS = 'os-flavor-access'
+authorize = extensions.soft_extension_authorizer('compute', 'v3:' + ALIAS)
+
+
+def make_flavor(elem):
+ elem.set('{%s}is_public' % FlavorAccess.namespace,
+ '%s:is_public' % FlavorAccess.alias)
+
+
+def make_flavor_access(elem):
+ elem.set('flavor_id')
+ elem.set('tenant_id')
+
+
+class FlavorTemplate(xmlutil.TemplateBuilder):
+ def construct(self):
+ root = xmlutil.TemplateElement('flavor', selector='flavor')
+ make_flavor(root)
+ alias = FlavorAccess.alias
+ namespace = FlavorAccess.namespace
+ return xmlutil.SlaveTemplate(root, 1, nsmap={alias: namespace})
+
+
+class FlavorsTemplate(xmlutil.TemplateBuilder):
+ def construct(self):
+ root = xmlutil.TemplateElement('flavors')
+ elem = xmlutil.SubTemplateElement(root, 'flavor', selector='flavors')
+ make_flavor(elem)
+ alias = FlavorAccess.alias
+ namespace = FlavorAccess.namespace
+ return xmlutil.SlaveTemplate(root, 1, nsmap={alias: namespace})
+
+
+class FlavorAccessTemplate(xmlutil.TemplateBuilder):
+ def construct(self):
+ root = xmlutil.TemplateElement('flavor_access')
+ elem = xmlutil.SubTemplateElement(root, 'access',
+ selector='flavor_access')
+ make_flavor_access(elem)
+ return xmlutil.MasterTemplate(root, 1)
+
+
+def _marshall_flavor_access(flavor_id):
+ rval = []
+ try:
+ access_list = flavors.\
+ get_flavor_access_by_flavor_id(flavor_id)
+ except exception.FlavorNotFound:
+ explanation = _("Flavor not found.")
+ raise webob.exc.HTTPNotFound(explanation=explanation)
+
+ for access in access_list:
+ rval.append({'flavor_id': flavor_id,
+ 'tenant_id': access['project_id']})
+
+ return {'flavor_access': rval}
+
+
+class FlavorAccessController(object):
+ """The flavor access API controller for the OpenStack API."""
+
+ def __init__(self):
+ super(FlavorAccessController, self).__init__()
+
+ @wsgi.serializers(xml=FlavorAccessTemplate)
+ def index(self, req, flavor_id):
+ context = req.environ['nova.context']
+ authorize(context)
+
+ try:
+ flavor = flavors.get_flavor_by_flavor_id(flavor_id)
+ except exception.FlavorNotFound:
+ explanation = _("Flavor not found.")
+ raise webob.exc.HTTPNotFound(explanation=explanation)
+
+ # public flavor to all projects
+ if flavor['is_public']:
+ explanation = _("Access list not available for public flavors.")
+ raise webob.exc.HTTPNotFound(explanation=explanation)
+
+ # private flavor to listed projects only
+ return _marshall_flavor_access(flavor_id)
+
+
+class FlavorActionController(wsgi.Controller):
+ """The flavor access API controller for the OpenStack API."""
+
+ def _check_body(self, body):
+ if body is None or body == "":
+ raise webob.exc.HTTPBadRequest(explanation=_("No request body"))
+
+ def _get_flavor_refs(self, context):
+ """Return a dictionary mapping flavorid to flavor_ref."""
+
+ flavor_refs = flavors.get_all_flavors(context)
+ rval = {}
+ for name, obj in flavor_refs.iteritems():
+ rval[obj['flavorid']] = obj
+ return rval
+
+ def _extend_flavor(self, flavor_rval, flavor_ref):
+ key = "%s:is_public" % (FlavorAccess.alias)
+ flavor_rval[key] = flavor_ref['is_public']
+
+ @wsgi.extends
+ def show(self, req, resp_obj, id):
+ context = req.environ['nova.context']
+ if authorize(context):
+ # Attach our slave template to the response object
+ resp_obj.attach(xml=FlavorTemplate())
+ db_flavor = req.get_db_flavor(id)
+
+ self._extend_flavor(resp_obj.obj['flavor'], db_flavor)
+
+ @wsgi.extends
+ def detail(self, req, resp_obj):
+ context = req.environ['nova.context']
+ if authorize(context):
+ # Attach our slave template to the response object
+ resp_obj.attach(xml=FlavorsTemplate())
+
+ flavors = list(resp_obj.obj['flavors'])
+ for flavor_rval in flavors:
+ db_flavor = req.get_db_flavor(flavor_rval['id'])
+ self._extend_flavor(flavor_rval, db_flavor)
+
+ @wsgi.extends(action='create')
+ def create(self, req, body, resp_obj):
+ context = req.environ['nova.context']
+ if authorize(context):
+ # Attach our slave template to the response object
+ resp_obj.attach(xml=FlavorTemplate())
+
+ db_flavor = req.get_db_flavor(resp_obj.obj['flavor']['id'])
+
+ self._extend_flavor(resp_obj.obj['flavor'], db_flavor)
+
+ @wsgi.serializers(xml=FlavorAccessTemplate)
+ @wsgi.action("addTenantAccess")
+ def _addTenantAccess(self, req, id, body):
+ context = req.environ['nova.context']
+ authorize(context)
+ self._check_body(body)
+
+ vals = body['addTenantAccess']
+ tenant = vals['tenant']
+
+ try:
+ flavors.add_flavor_access(id, tenant, context)
+ except exception.FlavorAccessExists as err:
+ raise webob.exc.HTTPConflict(explanation=err.format_message())
+
+ return _marshall_flavor_access(id)
+
+ @wsgi.serializers(xml=FlavorAccessTemplate)
+ @wsgi.action("removeTenantAccess")
+ def _removeTenantAccess(self, req, id, body):
+ context = req.environ['nova.context']
+ authorize(context)
+ self._check_body(body)
+
+ vals = body['removeTenantAccess']
+ tenant = vals['tenant']
+
+ try:
+ flavors.remove_flavor_access(id, tenant, context)
+ except exception.FlavorAccessNotFound as e:
+ raise webob.exc.HTTPNotFound(explanation=e.format_message())
+
+ return _marshall_flavor_access(id)
+
+
+class FlavorAccess(extensions.V3APIExtensionBase):
+ """Flavor access support."""
+
+ name = "FlavorAccess"
+ alias = ALIAS
+ namespace = "http://docs.openstack.org/compute/ext/%s/api/v3" % ALIAS
+ version = 1
+
+ def get_resources(self):
+ res = extensions.ResourceExtension(
+ ALIAS,
+ controller=FlavorAccessController(),
+ parent=dict(member_name='flavor', collection_name='flavors'))
+
+ return [res]
+
+ def get_controller_extensions(self):
+ extension = extensions.ControllerExtension(
+ self, 'flavors', FlavorActionController())
+
+ return [extension]
diff --git a/nova/api/openstack/compute/plugins/v3/flavor_disabled.py b/nova/api/openstack/compute/plugins/v3/flavor_disabled.py
new file mode 100644
index 000000000..79f6df9ce
--- /dev/null
+++ b/nova/api/openstack/compute/plugins/v3/flavor_disabled.py
@@ -0,0 +1,91 @@
+# Copyright 2012 Nebula, Inc.
+#
+# 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 Flavor Disabled API extension."""
+
+from nova.api.openstack import extensions
+from nova.api.openstack import wsgi
+from nova.api.openstack import xmlutil
+
+ALIAS = 'os-flavor-disabled'
+authorize = extensions.soft_extension_authorizer('compute', 'v3:' + ALIAS)
+
+
+class FlavorDisabledController(wsgi.Controller):
+ def _extend_flavors(self, req, flavors):
+ for flavor in flavors:
+ db_flavor = req.get_db_flavor(flavor['id'])
+ key = "%s:disabled" % FlavorDisabled.alias
+ flavor[key] = db_flavor['disabled']
+
+ def _show(self, req, resp_obj):
+ if not authorize(req.environ['nova.context']):
+ return
+ if 'flavor' in resp_obj.obj:
+ resp_obj.attach(xml=FlavorDisabledTemplate())
+ self._extend_flavors(req, [resp_obj.obj['flavor']])
+
+ @wsgi.extends
+ def show(self, req, resp_obj, id):
+ return self._show(req, resp_obj)
+
+ @wsgi.extends(action='create')
+ def create(self, req, resp_obj, body):
+ return self._show(req, resp_obj)
+
+ @wsgi.extends
+ def detail(self, req, resp_obj):
+ if not authorize(req.environ['nova.context']):
+ return
+ resp_obj.attach(xml=FlavorsDisabledTemplate())
+ self._extend_flavors(req, list(resp_obj.obj['flavors']))
+
+
+class FlavorDisabled(extensions.V3APIExtensionBase):
+ """Support to show the disabled status of a flavor."""
+
+ name = "FlavorDisabled"
+ alias = ALIAS
+ namespace = "http://docs.openstack.org/compute/ext/%s/api/v3" % ALIAS
+ version = 1
+
+ def get_controller_extensions(self):
+ controller = FlavorDisabledController()
+ extension = extensions.ControllerExtension(self, 'flavors', controller)
+ return [extension]
+
+ def get_resources(self):
+ return []
+
+
+def make_flavor(elem):
+ elem.set('{%s}disabled' % FlavorDisabled.namespace,
+ '%s:disabled' % FlavorDisabled.alias)
+
+
+class FlavorDisabledTemplate(xmlutil.TemplateBuilder):
+ def construct(self):
+ root = xmlutil.TemplateElement('flavor', selector='flavor')
+ make_flavor(root)
+ return xmlutil.SlaveTemplate(root, 1, nsmap={
+ FlavorDisabled.alias: FlavorDisabled.namespace})
+
+
+class FlavorsDisabledTemplate(xmlutil.TemplateBuilder):
+ def construct(self):
+ root = xmlutil.TemplateElement('flavors')
+ elem = xmlutil.SubTemplateElement(root, 'flavor', selector='flavors')
+ make_flavor(elem)
+ return xmlutil.SlaveTemplate(root, 1, nsmap={
+ FlavorDisabled.alias: FlavorDisabled.namespace})
diff --git a/nova/api/openstack/compute/plugins/v3/flavors.py b/nova/api/openstack/compute/plugins/v3/flavors.py
new file mode 100644
index 000000000..31c0fa8b7
--- /dev/null
+++ b/nova/api/openstack/compute/plugins/v3/flavors.py
@@ -0,0 +1,169 @@
+# 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 nova.api.openstack import common
+from nova.api.openstack.compute.views import flavors as flavors_view
+from nova.api.openstack import extensions
+from nova.api.openstack import wsgi
+from nova.api.openstack import xmlutil
+from nova.compute import flavors
+from nova import exception
+from nova.openstack.common import strutils
+
+
+def make_flavor(elem, detailed=False):
+ elem.set('name')
+ elem.set('id')
+ if detailed:
+ elem.set('ram')
+ elem.set('disk')
+ elem.set('vcpus', xmlutil.EmptyStringSelector('vcpus'))
+ # NOTE(vish): this was originally added without a namespace
+ elem.set('swap', xmlutil.EmptyStringSelector('swap'))
+
+ xmlutil.make_links(elem, 'links')
+
+
+flavor_nsmap = {None: xmlutil.XMLNS_V11, 'atom': xmlutil.XMLNS_ATOM}
+
+
+class FlavorTemplate(xmlutil.TemplateBuilder):
+ def construct(self):
+ root = xmlutil.TemplateElement('flavor', selector='flavor')
+ make_flavor(root, detailed=True)
+ return xmlutil.MasterTemplate(root, 1, nsmap=flavor_nsmap)
+
+
+class MinimalFlavorsTemplate(xmlutil.TemplateBuilder):
+ def construct(self):
+ root = xmlutil.TemplateElement('flavors')
+ elem = xmlutil.SubTemplateElement(root, 'flavor', selector='flavors')
+ make_flavor(elem)
+ return xmlutil.MasterTemplate(root, 1, nsmap=flavor_nsmap)
+
+
+class FlavorsTemplate(xmlutil.TemplateBuilder):
+ def construct(self):
+ root = xmlutil.TemplateElement('flavors')
+ elem = xmlutil.SubTemplateElement(root, 'flavor', selector='flavors')
+ make_flavor(elem, detailed=True)
+ return xmlutil.MasterTemplate(root, 1, nsmap=flavor_nsmap)
+
+
+class FlavorsController(wsgi.Controller):
+ """Flavor controller for the OpenStack API."""
+
+ _view_builder_class = flavors_view.ViewBuilder
+
+ @wsgi.serializers(xml=MinimalFlavorsTemplate)
+ def index(self, req):
+ """Return all flavors in brief."""
+ limited_flavors = self._get_flavors(req)
+ return self._view_builder.index(req, limited_flavors)
+
+ @wsgi.serializers(xml=FlavorsTemplate)
+ def detail(self, req):
+ """Return all flavors in detail."""
+ limited_flavors = self._get_flavors(req)
+ req.cache_db_flavors(limited_flavors)
+ return self._view_builder.detail(req, limited_flavors)
+
+ @wsgi.serializers(xml=FlavorTemplate)
+ def show(self, req, id):
+ """Return data about the given flavor id."""
+ try:
+ flavor = flavors.get_flavor_by_flavor_id(id)
+ req.cache_db_flavor(flavor)
+ except exception.NotFound:
+ raise webob.exc.HTTPNotFound()
+
+ return self._view_builder.show(req, flavor)
+
+ def _parse_is_public(self, is_public):
+ """Parse is_public into something usable."""
+
+ if is_public is None:
+ # preserve default value of showing only public flavors
+ return True
+ elif is_public == 'none':
+ return None
+ else:
+ try:
+ return strutils.bool_from_string(is_public, strict=True)
+ except ValueError:
+ msg = _('Invalid is_public filter [%s]') % is_public
+ raise webob.exc.HTTPBadRequest(explanation=msg)
+
+ def _get_flavors(self, req):
+ """Helper function that returns a list of flavor dicts."""
+ filters = {}
+
+ context = req.environ['nova.context']
+ if context.is_admin:
+ # Only admin has query access to all flavor types
+ filters['is_public'] = self._parse_is_public(
+ req.params.get('is_public', None))
+ else:
+ filters['is_public'] = True
+ filters['disabled'] = False
+
+ if 'minRam' in req.params:
+ try:
+ filters['min_memory_mb'] = int(req.params['minRam'])
+ except ValueError:
+ msg = _('Invalid minRam filter [%s]') % req.params['minRam']
+ raise webob.exc.HTTPBadRequest(explanation=msg)
+
+ if 'minDisk' in req.params:
+ try:
+ filters['min_root_gb'] = int(req.params['minDisk'])
+ except ValueError:
+ msg = _('Invalid minDisk filter [%s]') % req.params['minDisk']
+ raise webob.exc.HTTPBadRequest(explanation=msg)
+
+ limited_flavors = flavors.get_all_flavors(context, filters=filters)
+ flavors_list = limited_flavors.values()
+ sorted_flavors = sorted(flavors_list,
+ key=lambda item: item['flavorid'])
+ limited_flavors = common.limited_by_marker(sorted_flavors, req)
+ return limited_flavors
+
+
+class Flavors(extensions.V3APIExtensionBase):
+ """ Flavors Extension. """
+ name = "flavors"
+ alias = "flavors"
+ namespace = "http://docs.openstack.org/compute/core/flavors/v3"
+ version = 1
+
+ def get_resources(self):
+ collection_actions = {'detail': 'GET'}
+ member_actions = {'action': 'POST'}
+
+ resources = [
+ extensions.ResourceExtension('flavors',
+ FlavorsController(),
+ member_name='flavor',
+ collection_actions=collection_actions,
+ member_actions=member_actions)
+ ]
+ return resources
+
+ def get_controller_extensions(self):
+ return []
diff --git a/nova/api/openstack/compute/plugins/v3/images.py b/nova/api/openstack/compute/plugins/v3/images.py
new file mode 100644
index 000000000..dde22488d
--- /dev/null
+++ b/nova/api/openstack/compute/plugins/v3/images.py
@@ -0,0 +1,242 @@
+# Copyright 2011 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.exc
+
+from nova.api.openstack import common
+from nova.api.openstack.compute.views import images as views_images
+from nova.api.openstack import extensions
+from nova.api.openstack import wsgi
+from nova.api.openstack import xmlutil
+from nova import exception
+import nova.image.glance
+import nova.utils
+
+
+ALIAS = "os-images"
+authorize = extensions.extension_authorizer('compute', 'v3:' + ALIAS)
+
+SUPPORTED_FILTERS = {
+ 'name': 'name',
+ 'status': 'status',
+ 'changes-since': 'changes-since',
+ 'server': 'property-instance_uuid',
+ 'type': 'property-image_type',
+ 'minRam': 'min_ram',
+ 'minDisk': 'min_disk',
+}
+
+
+def make_image(elem, detailed=False):
+ elem.set('name')
+ elem.set('id')
+
+ if detailed:
+ elem.set('updated')
+ elem.set('created')
+ elem.set('status')
+ elem.set('progress')
+ elem.set('minRam')
+ elem.set('minDisk')
+
+ server = xmlutil.SubTemplateElement(elem, 'server', selector='server')
+ server.set('id')
+ xmlutil.make_links(server, 'links')
+
+ elem.append(common.MetadataTemplate())
+
+ xmlutil.make_links(elem, 'links')
+
+
+image_nsmap = {None: xmlutil.XMLNS_V11, 'atom': xmlutil.XMLNS_ATOM}
+
+
+class ImageTemplate(xmlutil.TemplateBuilder):
+ def construct(self):
+ root = xmlutil.TemplateElement('image', selector='image')
+ make_image(root, detailed=True)
+ return xmlutil.MasterTemplate(root, 1, nsmap=image_nsmap)
+
+
+class MinimalImagesTemplate(xmlutil.TemplateBuilder):
+ def construct(self):
+ root = xmlutil.TemplateElement('images')
+ elem = xmlutil.SubTemplateElement(root, 'image', selector='images')
+ make_image(elem)
+ xmlutil.make_links(root, 'images_links')
+ return xmlutil.MasterTemplate(root, 1, nsmap=image_nsmap)
+
+
+class ImagesTemplate(xmlutil.TemplateBuilder):
+ def construct(self):
+ root = xmlutil.TemplateElement('images')
+ elem = xmlutil.SubTemplateElement(root, 'image', selector='images')
+ make_image(elem, detailed=True)
+ return xmlutil.MasterTemplate(root, 1, nsmap=image_nsmap)
+
+
+class ImagesController(wsgi.Controller):
+ """Base controller for retrieving/displaying images."""
+
+ _view_builder_class = views_images.ViewBuilder
+
+ def __init__(self, image_service=None, **kwargs):
+ """Initialize new `ImageController`.
+
+ :param image_service: `nova.image.glance:GlanceImageService`
+
+ """
+ super(ImagesController, self).__init__(**kwargs)
+ self._image_service = (image_service or
+ nova.image.glance.get_default_image_service())
+
+ def _get_filters(self, req):
+ """
+ Return a dictionary of query param filters from the request
+
+ :param req: the Request object coming from the wsgi layer
+ :retval a dict of key/value filters
+ """
+ filters = {}
+ for param in req.params:
+ if param in SUPPORTED_FILTERS or param.startswith('property-'):
+ # map filter name or carry through if property-*
+ filter_name = SUPPORTED_FILTERS.get(param, param)
+ filters[filter_name] = req.params.get(param)
+
+ # ensure server filter is the instance uuid
+ filter_name = 'property-instance_uuid'
+ try:
+ filters[filter_name] = filters[filter_name].rsplit('/', 1)[1]
+ except (AttributeError, IndexError, KeyError):
+ pass
+
+ filter_name = 'status'
+ if filter_name in filters:
+ # The Image API expects us to use lowercase strings for status
+ filters[filter_name] = filters[filter_name].lower()
+
+ return filters
+
+ @wsgi.serializers(xml=ImageTemplate)
+ def show(self, req, id):
+ """Return detailed information about a specific image.
+
+ :param req: `wsgi.Request` object
+ :param id: Image identifier
+ """
+ context = req.environ['nova.context']
+ authorize(context)
+
+ try:
+ image = self._image_service.show(context, id)
+ except (exception.NotFound, exception.InvalidImageRef):
+ explanation = _("Image not found.")
+ raise webob.exc.HTTPNotFound(explanation=explanation)
+
+ req.cache_db_items('images', [image], 'id')
+ return self._view_builder.show(req, image)
+
+ def delete(self, req, id):
+ """Delete an image, if allowed.
+
+ :param req: `wsgi.Request` object
+ :param id: Image identifier (integer)
+ """
+ context = req.environ['nova.context']
+ authorize(context)
+
+ try:
+ self._image_service.delete(context, id)
+ except exception.ImageNotFound:
+ explanation = _("Image not found.")
+ raise webob.exc.HTTPNotFound(explanation=explanation)
+ except exception.ImageNotAuthorized:
+ # The image service raises this exception on delete if glanceclient
+ # raises HTTPForbidden.
+ explanation = _("You are not allowed to delete the image.")
+ raise webob.exc.HTTPForbidden(explanation=explanation)
+ return webob.exc.HTTPNoContent()
+
+ @wsgi.serializers(xml=MinimalImagesTemplate)
+ def index(self, req):
+ """Return an index listing of images available to the request.
+
+ :param req: `wsgi.Request` object
+
+ """
+ context = req.environ['nova.context']
+ authorize(context)
+
+ filters = self._get_filters(req)
+ params = req.GET.copy()
+ page_params = common.get_pagination_params(req)
+ for key, val in page_params.iteritems():
+ params[key] = val
+
+ try:
+ images = self._image_service.detail(context, filters=filters,
+ **page_params)
+ except exception.Invalid as e:
+ raise webob.exc.HTTPBadRequest(explanation=e.format_message())
+ return self._view_builder.index(req, images)
+
+ @wsgi.serializers(xml=ImagesTemplate)
+ def detail(self, req):
+ """Return a detailed index listing of images available to the request.
+
+ :param req: `wsgi.Request` object.
+
+ """
+ context = req.environ['nova.context']
+ authorize(context)
+
+ filters = self._get_filters(req)
+ params = req.GET.copy()
+ page_params = common.get_pagination_params(req)
+ for key, val in page_params.iteritems():
+ params[key] = val
+ try:
+ images = self._image_service.detail(context, filters=filters,
+ **page_params)
+ except exception.Invalid as e:
+ raise webob.exc.HTTPBadRequest(explanation=e.format_message())
+
+ req.cache_db_items('images', images, 'id')
+ return self._view_builder.detail(req, images)
+
+ def create(self, *args, **kwargs):
+ raise webob.exc.HTTPMethodNotAllowed()
+
+
+class Images(extensions.V3APIExtensionBase):
+ """Server addresses."""
+
+ name = "Images"
+ alias = ALIAS
+ namespace = "http://docs.openstack.org/compute/ext/images/v3"
+ version = 1
+
+ def get_resources(self):
+ collection_actions = {'detail': 'GET'}
+ resources = [
+ extensions.ResourceExtension(
+ ALIAS, ImagesController(),
+ collection_actions=collection_actions)]
+
+ return resources
+
+ def get_controller_extensions(self):
+ return []
diff --git a/nova/api/openstack/compute/plugins/v3/quota_classes.py b/nova/api/openstack/compute/plugins/v3/quota_classes.py
new file mode 100644
index 000000000..361748df8
--- /dev/null
+++ b/nova/api/openstack/compute/plugins/v3/quota_classes.py
@@ -0,0 +1,103 @@
+# Copyright 2012 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 nova.api.openstack import extensions
+from nova.api.openstack import wsgi
+from nova.api.openstack import xmlutil
+import nova.context
+from nova import db
+from nova import exception
+from nova import quota
+
+
+QUOTAS = quota.QUOTAS
+
+
+authorize = extensions.extension_authorizer('compute', 'quota_classes')
+
+
+class QuotaClassTemplate(xmlutil.TemplateBuilder):
+ def construct(self):
+ root = xmlutil.TemplateElement('quota_class_set',
+ selector='quota_class_set')
+ root.set('id')
+
+ for resource in QUOTAS.resources:
+ elem = xmlutil.SubTemplateElement(root, resource)
+ elem.text = resource
+
+ return xmlutil.MasterTemplate(root, 1)
+
+
+class QuotaClassSetsController(object):
+
+ def _format_quota_set(self, quota_class, quota_set):
+ """Convert the quota object to a result dict."""
+
+ result = dict(id=str(quota_class))
+
+ for resource in QUOTAS.resources:
+ result[resource] = quota_set[resource]
+
+ return dict(quota_class_set=result)
+
+ @wsgi.serializers(xml=QuotaClassTemplate)
+ def show(self, req, id):
+ context = req.environ['nova.context']
+ authorize(context)
+ try:
+ nova.context.authorize_quota_class_context(context, id)
+ return self._format_quota_set(id,
+ QUOTAS.get_class_quotas(context, id))
+ except exception.NotAuthorized:
+ raise webob.exc.HTTPForbidden()
+
+ @wsgi.serializers(xml=QuotaClassTemplate)
+ def update(self, req, id, body):
+ context = req.environ['nova.context']
+ authorize(context)
+ quota_class = id
+ for key in body['quota_class_set'].keys():
+ if key in QUOTAS:
+ value = int(body['quota_class_set'][key])
+ try:
+ db.quota_class_update(context, quota_class, key, value)
+ except exception.QuotaClassNotFound:
+ db.quota_class_create(context, quota_class, key, value)
+ except exception.AdminRequired:
+ raise webob.exc.HTTPForbidden()
+ return {'quota_class_set': QUOTAS.get_class_quotas(context,
+ quota_class)}
+
+
+class Quota_classes(extensions.ExtensionDescriptor):
+ """Quota classes management support."""
+
+ name = "QuotaClasses"
+ alias = "os-quota-class-sets"
+ namespace = ("http://docs.openstack.org/compute/ext/"
+ "quota-classes-sets/api/v1.1")
+ updated = "2012-03-12T00:00:00+00:00"
+
+ def get_resources(self):
+ resources = []
+
+ res = extensions.ResourceExtension('os-quota-class-sets',
+ QuotaClassSetsController())
+ resources.append(res)
+
+ return resources
diff --git a/nova/api/openstack/compute/plugins/v3/server_diagnostics.py b/nova/api/openstack/compute/plugins/v3/server_diagnostics.py
new file mode 100644
index 000000000..6a19732dc
--- /dev/null
+++ b/nova/api/openstack/compute/plugins/v3/server_diagnostics.py
@@ -0,0 +1,71 @@
+# Copyright 2011 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.exc
+
+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
+
+
+ALIAS = "os-server-diagnostics"
+authorize = extensions.extension_authorizer('compute', 'v3:' + ALIAS)
+sd_nsmap = {None: wsgi.XMLNS_V11}
+
+
+class ServerDiagnosticsTemplate(xmlutil.TemplateBuilder):
+ def construct(self):
+ root = xmlutil.TemplateElement('diagnostics')
+ elem = xmlutil.SubTemplateElement(root, xmlutil.Selector(0),
+ selector=xmlutil.get_items)
+ elem.text = 1
+ return xmlutil.MasterTemplate(root, 1, nsmap=sd_nsmap)
+
+
+class ServerDiagnosticsController(object):
+ @wsgi.serializers(xml=ServerDiagnosticsTemplate)
+ def index(self, req, server_id):
+ context = req.environ["nova.context"]
+ authorize(context)
+ compute_api = compute.API()
+ try:
+ instance = compute_api.get(context, server_id)
+ except exception.NotFound():
+ raise webob.exc.HTTPNotFound(_("Instance not found"))
+
+ return compute_api.get_diagnostics(context, instance)
+
+
+class ServerDiagnostics(extensions.V3APIExtensionBase):
+ """Allow Admins to view server diagnostics through server action."""
+
+ name = "ServerDiagnostics"
+ alias = ALIAS
+ namespace = ("http://docs.openstack.org/compute/ext/"
+ "server-diagnostics/api/v3")
+ version = 1
+
+ def get_resources(self):
+ parent_def = {'member_name': 'server', 'collection_name': 'servers'}
+ resources = [
+ extensions.ResourceExtension(ALIAS,
+ ServerDiagnosticsController(),
+ parent=parent_def)]
+ return resources
+
+ def get_controller_extensions(self):
+ return []