diff options
Diffstat (limited to 'nova/api')
-rw-r--r-- | nova/api/metadata/base.py | 6 | ||||
-rw-r--r-- | nova/api/openstack/common.py | 16 | ||||
-rw-r--r-- | nova/api/openstack/compute/contrib/extended_availability_zone.py | 11 | ||||
-rw-r--r-- | nova/api/openstack/compute/flavors.py | 3 | ||||
-rw-r--r-- | nova/api/openstack/compute/limits.py | 34 | ||||
-rw-r--r-- | nova/api/openstack/compute/plugins/v3/cells.py | 347 | ||||
-rw-r--r-- | nova/api/openstack/compute/plugins/v3/flavor_access.py | 220 | ||||
-rw-r--r-- | nova/api/openstack/compute/plugins/v3/flavor_disabled.py | 91 | ||||
-rw-r--r-- | nova/api/openstack/compute/plugins/v3/flavors.py | 169 | ||||
-rw-r--r-- | nova/api/openstack/compute/plugins/v3/images.py | 242 | ||||
-rw-r--r-- | nova/api/openstack/compute/plugins/v3/quota_classes.py | 103 | ||||
-rw-r--r-- | nova/api/openstack/compute/plugins/v3/server_diagnostics.py | 71 | ||||
-rw-r--r-- | nova/api/openstack/compute/schemas/v3/flavor.rng | 12 | ||||
-rw-r--r-- | nova/api/openstack/compute/schemas/v3/flavors.rng | 6 | ||||
-rw-r--r-- | nova/api/openstack/compute/servers.py | 3 | ||||
-rw-r--r-- | nova/api/openstack/compute/views/servers.py | 4 | ||||
-rw-r--r-- | nova/api/openstack/xmlutil.py | 8 |
17 files changed, 1307 insertions, 39 deletions
diff --git a/nova/api/metadata/base.py b/nova/api/metadata/base.py index 425e5bf53..da0a50662 100644 --- a/nova/api/metadata/base.py +++ b/nova/api/metadata/base.py @@ -32,6 +32,8 @@ from nova.compute import flavors from nova import conductor from nova import context from nova import network +from nova import utils + from nova.openstack.common import timeutils from nova.virt import netutils @@ -127,9 +129,7 @@ class InstanceMetadata(): self.address = address # expose instance metadata. - self.launch_metadata = {} - for item in instance.get('metadata', []): - self.launch_metadata[item['key']] = item['value'] + self.launch_metadata = utils.instance_meta(instance) self.password = password.extract_password(instance) diff --git a/nova/api/openstack/common.py b/nova/api/openstack/common.py index dd746e23d..509256d87 100644 --- a/nova/api/openstack/common.py +++ b/nova/api/openstack/common.py @@ -457,6 +457,16 @@ def check_snapshots_enabled(f): class ViewBuilder(object): """Model API responses as dictionaries.""" + def _get_project_id(self, request): + """ + Get project id from request url if present or empty string + otherwise + """ + project_id = request.environ["nova.context"].project_id + if project_id in request.url: + return project_id + return '' + def _get_links(self, request, identifier, collection_name): return [{ "rel": "self", @@ -475,7 +485,7 @@ class ViewBuilder(object): params["marker"] = identifier prefix = self._update_compute_link_prefix(request.application_url) url = os.path.join(prefix, - request.environ["nova.context"].project_id, + self._get_project_id(request), collection_name) return "%s?%s" % (url, dict_to_query_str(params)) @@ -483,7 +493,7 @@ class ViewBuilder(object): """Return an href string pointing to this object.""" prefix = self._update_compute_link_prefix(request.application_url) return os.path.join(prefix, - request.environ["nova.context"].project_id, + self._get_project_id(request), collection_name, str(identifier)) @@ -492,7 +502,7 @@ class ViewBuilder(object): base_url = remove_version_from_href(request.application_url) base_url = self._update_compute_link_prefix(base_url) return os.path.join(base_url, - request.environ["nova.context"].project_id, + self._get_project_id(request), collection_name, str(identifier)) diff --git a/nova/api/openstack/compute/contrib/extended_availability_zone.py b/nova/api/openstack/compute/contrib/extended_availability_zone.py index 00765e209..6e77cd6bd 100644 --- a/nova/api/openstack/compute/contrib/extended_availability_zone.py +++ b/nova/api/openstack/compute/contrib/extended_availability_zone.py @@ -20,7 +20,7 @@ from nova.api.openstack import extensions from nova.api.openstack import wsgi from nova.api.openstack import xmlutil -from nova import availability_zones +from nova import availability_zones as avail_zone authorize = extensions.soft_extension_authorizer('compute', 'extended_availability_zone') @@ -29,8 +29,13 @@ authorize = extensions.soft_extension_authorizer('compute', class ExtendedAZController(wsgi.Controller): def _extend_server(self, context, server, instance): key = "%s:availability_zone" % Extended_availability_zone.alias - server[key] = availability_zones.get_instance_availability_zone( - context, instance) + az = avail_zone.get_instance_availability_zone(context, instance) + if not az and instance.get('availability_zone'): + # Likely hasn't reached a viable compute node yet so give back the + # desired availability_zone that *may* exist in the instance + # record itself. + az = instance['availability_zone'] + server[key] = az @wsgi.extends def show(self, req, resp_obj, id): diff --git a/nova/api/openstack/compute/flavors.py b/nova/api/openstack/compute/flavors.py index 4da130e9b..c6303646a 100644 --- a/nova/api/openstack/compute/flavors.py +++ b/nova/api/openstack/compute/flavors.py @@ -24,6 +24,7 @@ from nova.api.openstack import xmlutil from nova.compute import flavors from nova import exception from nova.openstack.common import strutils +from nova import utils def make_flavor(elem, detailed=False): @@ -98,7 +99,7 @@ class Controller(wsgi.Controller): if is_public is None: # preserve default value of showing only public flavors return True - elif is_public == 'none': + elif utils.is_none_string(is_public): return None else: try: diff --git a/nova/api/openstack/compute/limits.py b/nova/api/openstack/compute/limits.py index 07e791306..107f40436 100644 --- a/nova/api/openstack/compute/limits.py +++ b/nova/api/openstack/compute/limits.py @@ -33,19 +33,13 @@ from nova.api.openstack import xmlutil from nova.openstack.common import importutils from nova.openstack.common import jsonutils from nova import quota +from nova import utils from nova import wsgi as base_wsgi QUOTAS = quota.QUOTAS -# Convenience constants for the limits dictionary passed to Limiter(). -PER_SECOND = 1 -PER_MINUTE = 60 -PER_HOUR = 60 * 60 -PER_DAY = 60 * 60 * 24 - - limits_nsmap = {None: xmlutil.XMLNS_COMMON_V10, 'atom': xmlutil.XMLNS_ATOM} @@ -122,14 +116,7 @@ class Limit(object): Stores information about a limit for HTTP requests. """ - UNITS = { - 1: "SECOND", - 60: "MINUTE", - 60 * 60: "HOUR", - 60 * 60 * 24: "DAY", - } - - UNIT_MAP = dict([(v, k) for k, v in UNITS.items()]) + UNITS = dict([(v, k) for k, v in utils.TIME_UNITS.items()]) def __init__(self, verb, uri, regex, value, unit): """ @@ -223,12 +210,13 @@ class Limit(object): # a regular-expression to match, value and unit of measure (PER_DAY, etc.) DEFAULT_LIMITS = [ - Limit("POST", "*", ".*", 10, PER_MINUTE), - Limit("POST", "*/servers", "^/servers", 50, PER_DAY), - Limit("PUT", "*", ".*", 10, PER_MINUTE), - Limit("GET", "*changes-since*", ".*changes-since.*", 3, PER_MINUTE), - Limit("DELETE", "*", ".*", 100, PER_MINUTE), - Limit("GET", "*/os-fping", "^/os-fping", 12, PER_HOUR), + Limit("POST", "*", ".*", 10, utils.TIME_UNITS['MINUTE']), + Limit("POST", "*/servers", "^/servers", 50, utils.TIME_UNITS['DAY']), + Limit("PUT", "*", ".*", 10, utils.TIME_UNITS['MINUTE']), + Limit("GET", "*changes-since*", ".*changes-since.*", 3, + utils.TIME_UNITS['MINUTE']), + Limit("DELETE", "*", ".*", 100, utils.TIME_UNITS['MINUTE']), + Limit("GET", "*/os-fping", "^/os-fping", 12, utils.TIME_UNITS['HOUR']), ] @@ -390,9 +378,9 @@ class Limiter(object): # Convert unit unit = unit.upper() - if unit not in Limit.UNIT_MAP: + if unit not in utils.TIME_UNITS: raise ValueError("Invalid units specified") - unit = Limit.UNIT_MAP[unit] + unit = utils.TIME_UNITS[unit] # Build a limit result.append(Limit(verb, uri, regex, value, unit)) 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 [] diff --git a/nova/api/openstack/compute/schemas/v3/flavor.rng b/nova/api/openstack/compute/schemas/v3/flavor.rng new file mode 100644 index 000000000..4b6b74001 --- /dev/null +++ b/nova/api/openstack/compute/schemas/v3/flavor.rng @@ -0,0 +1,12 @@ +<element name="flavor" ns="http://docs.openstack.org/compute/api/v1.1" + xmlns="http://relaxng.org/ns/structure/1.0"> + <attribute name="name"> <text/> </attribute> + <attribute name="id"> <text/> </attribute> + <attribute name="ram"> <text/> </attribute> + <attribute name="disk"> <text/> </attribute> + <attribute name="vcpus"> <text/> </attribute> + <attribute name="swap"> <text/> </attribute> + <zeroOrMore> + <externalRef href="../atom-link.rng"/> + </zeroOrMore> +</element> diff --git a/nova/api/openstack/compute/schemas/v3/flavors.rng b/nova/api/openstack/compute/schemas/v3/flavors.rng new file mode 100644 index 000000000..b7a3acc01 --- /dev/null +++ b/nova/api/openstack/compute/schemas/v3/flavors.rng @@ -0,0 +1,6 @@ +<element name="flavors" xmlns="http://relaxng.org/ns/structure/1.0" + ns="http://docs.openstack.org/compute/api/v1.1"> + <zeroOrMore> + <externalRef href="flavor.rng"/> + </zeroOrMore> +</element> diff --git a/nova/api/openstack/compute/servers.py b/nova/api/openstack/compute/servers.py index b9e18b501..8c329d12b 100644 --- a/nova/api/openstack/compute/servers.py +++ b/nova/api/openstack/compute/servers.py @@ -919,6 +919,9 @@ class Controller(wsgi.Controller): except exception.KeypairNotFound as error: msg = _("Invalid key_name provided.") raise exc.HTTPBadRequest(explanation=msg) + except exception.ConfigDriveInvalidValue: + msg = _("Invalid config_drive provided.") + raise exc.HTTPBadRequest(explanation=msg) except rpc_common.RemoteError as err: msg = "%(err_type)s: %(err_msg)s" % {'err_type': err.exc_type, 'err_msg': err.value} diff --git a/nova/api/openstack/compute/views/servers.py b/nova/api/openstack/compute/views/servers.py index 734bb647d..03b0a97a5 100644 --- a/nova/api/openstack/compute/views/servers.py +++ b/nova/api/openstack/compute/views/servers.py @@ -25,6 +25,7 @@ from nova.api.openstack.compute.views import images as views_images from nova.compute import flavors from nova.openstack.common import log as logging from nova.openstack.common import timeutils +from nova import utils LOG = logging.getLogger(__name__) @@ -132,8 +133,7 @@ class ViewBuilder(common.ViewBuilder): @staticmethod def _get_metadata(instance): - metadata = instance.get("metadata", []) - return dict((item['key'], item['value']) for item in metadata) + return utils.instance_meta(instance) @staticmethod def _get_vm_state(instance): diff --git a/nova/api/openstack/xmlutil.py b/nova/api/openstack/xmlutil.py index 04f5e28e3..37766b3e3 100644 --- a/nova/api/openstack/xmlutil.py +++ b/nova/api/openstack/xmlutil.py @@ -33,12 +33,12 @@ XMLNS_COMMON_V10 = 'http://docs.openstack.org/common/api/v1.0' XMLNS_ATOM = 'http://www.w3.org/2005/Atom' -def validate_schema(xml, schema_name): +def validate_schema(xml, schema_name, version='v1.1'): if isinstance(xml, str): xml = etree.fromstring(xml) - base_path = 'nova/api/openstack/compute/schemas/v1.1/' - if schema_name in ('atom', 'atom-link'): - base_path = 'nova/api/openstack/compute/schemas/' + base_path = 'nova/api/openstack/compute/schemas/' + if schema_name not in ('atom', 'atom-link'): + base_path += '%s/' % version schema_path = os.path.join(utils.novadir(), '%s%s.rng' % (base_path, schema_name)) schema_doc = etree.parse(schema_path) |