diff options
Diffstat (limited to 'nova/api/openstack/compute/plugins')
-rw-r--r-- | nova/api/openstack/compute/plugins/v3/agents.py | 168 | ||||
-rw-r--r-- | nova/api/openstack/compute/plugins/v3/cells.py | 347 | ||||
-rw-r--r-- | nova/api/openstack/compute/plugins/v3/certificates.py | 97 | ||||
-rw-r--r-- | nova/api/openstack/compute/plugins/v3/evacuate.py | 101 | ||||
-rw-r--r-- | nova/api/openstack/compute/plugins/v3/extension_info.py | 23 | ||||
-rw-r--r-- | nova/api/openstack/compute/plugins/v3/fixed_ips.py | 2 | ||||
-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/keypairs.py | 3 | ||||
-rw-r--r-- | nova/api/openstack/compute/plugins/v3/quota_classes.py | 103 | ||||
-rw-r--r-- | nova/api/openstack/compute/plugins/v3/quota_sets.py | 214 | ||||
-rw-r--r-- | nova/api/openstack/compute/plugins/v3/rescue.py | 100 | ||||
-rw-r--r-- | nova/api/openstack/compute/plugins/v3/server_diagnostics.py | 71 |
15 files changed, 1949 insertions, 2 deletions
diff --git a/nova/api/openstack/compute/plugins/v3/agents.py b/nova/api/openstack/compute/plugins/v3/agents.py new file mode 100644 index 000000000..02e752dac --- /dev/null +++ b/nova/api/openstack/compute/plugins/v3/agents.py @@ -0,0 +1,168 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012 IBM Corp. +# +# 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 db +from nova import exception + + +authorize = extensions.extension_authorizer('compute', 'agents') + + +class AgentsIndexTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('agents') + elem = xmlutil.SubTemplateElement(root, 'agent', selector='agents') + elem.set('hypervisor') + elem.set('os') + elem.set('architecture') + elem.set('version') + elem.set('md5hash') + elem.set('agent_id') + elem.set('url') + + return xmlutil.MasterTemplate(root, 1) + + +class AgentController(object): + """ + The agent is talking about guest agent.The host can use this for + things like accessing files on the disk, configuring networking, + or running other applications/scripts in the guest while it is + running. Typically this uses some hypervisor-specific transport + to avoid being dependent on a working network configuration. + Xen, VMware, and VirtualBox have guest agents,although the Xen + driver is the only one with an implementation for managing them + in openstack. KVM doesn't really have a concept of a guest agent + (although one could be written). + + You can find the design of agent update in this link: + http://wiki.openstack.org/AgentUpdate + and find the code in nova.virt.xenapi.vmops.VMOps._boot_new_instance. + In this design We need update agent in guest from host, so we need + some interfaces to update the agent info in host. + + You can find more information about the design of the GuestAgent in + the following link: + http://wiki.openstack.org/GuestAgent + http://wiki.openstack.org/GuestAgentXenStoreCommunication + """ + @wsgi.serializers(xml=AgentsIndexTemplate) + def index(self, req): + """ + Return a list of all agent builds. Filter by hypervisor. + """ + context = req.environ['nova.context'] + authorize(context) + hypervisor = None + agents = [] + if 'hypervisor' in req.GET: + hypervisor = req.GET['hypervisor'] + + for agent_build in db.agent_build_get_all(context, hypervisor): + agents.append({'hypervisor': agent_build.hypervisor, + 'os': agent_build.os, + 'architecture': agent_build.architecture, + 'version': agent_build.version, + 'md5hash': agent_build.md5hash, + 'agent_id': agent_build.id, + 'url': agent_build.url}) + + return {'agents': agents} + + def update(self, req, id, body): + """Update an existing agent build.""" + context = req.environ['nova.context'] + authorize(context) + + try: + para = body['para'] + url = para['url'] + md5hash = para['md5hash'] + version = para['version'] + except (TypeError, KeyError): + raise webob.exc.HTTPUnprocessableEntity() + + try: + db.agent_build_update(context, id, + {'version': version, + 'url': url, + 'md5hash': md5hash}) + except exception.AgentBuildNotFound as ex: + raise webob.exc.HTTPNotFound(explanation=ex.format_message()) + + return {"agent": {'agent_id': id, 'version': version, + 'url': url, 'md5hash': md5hash}} + + def delete(self, req, id): + """Deletes an existing agent build.""" + context = req.environ['nova.context'] + authorize(context) + + try: + db.agent_build_destroy(context, id) + except exception.AgentBuildNotFound as ex: + raise webob.exc.HTTPNotFound(explanation=ex.format_message()) + + def create(self, req, body): + """Creates a new agent build.""" + context = req.environ['nova.context'] + authorize(context) + + try: + agent = body['agent'] + hypervisor = agent['hypervisor'] + os = agent['os'] + architecture = agent['architecture'] + version = agent['version'] + url = agent['url'] + md5hash = agent['md5hash'] + except (TypeError, KeyError): + raise webob.exc.HTTPUnprocessableEntity() + + try: + agent_build_ref = db.agent_build_create(context, + {'hypervisor': hypervisor, + 'os': os, + 'architecture': architecture, + 'version': version, + 'url': url, + 'md5hash': md5hash}) + agent['agent_id'] = agent_build_ref.id + except Exception as ex: + raise webob.exc.HTTPServerError(str(ex)) + return {'agent': agent} + + +class Agents(extensions.ExtensionDescriptor): + """Agents support.""" + + name = "Agents" + alias = "os-agents" + namespace = "http://docs.openstack.org/compute/ext/agents/api/v2" + updated = "2012-10-28T00:00:00-00:00" + + def get_resources(self): + resources = [] + resource = extensions.ResourceExtension('os-agents', + AgentController()) + resources.append(resource) + return resources 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/certificates.py b/nova/api/openstack/compute/plugins/v3/certificates.py new file mode 100644 index 000000000..175780f9c --- /dev/null +++ b/nova/api/openstack/compute/plugins/v3/certificates.py @@ -0,0 +1,97 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright (c) 2012 OpenStack Foundation +# +# 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 +import nova.cert.rpcapi +from nova import network + +ALIAS = "os-certificates" +authorize = extensions.extension_authorizer('compute', 'v3:' + ALIAS) + + +def make_certificate(elem): + elem.set('data') + elem.set('private_key') + + +class CertificateTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('certificate', + selector='certificate') + make_certificate(root) + return xmlutil.MasterTemplate(root, 1) + + +def _translate_certificate_view(certificate, private_key=None): + return { + 'data': certificate, + 'private_key': private_key, + } + + +class CertificatesController(object): + """The x509 Certificates API controller for the OpenStack API.""" + + def __init__(self): + self.network_api = network.API() + self.cert_rpcapi = nova.cert.rpcapi.CertAPI() + super(CertificatesController, self).__init__() + + @wsgi.serializers(xml=CertificateTemplate) + def show(self, req, id): + """Return certificate information.""" + context = req.environ['nova.context'] + authorize(context) + if id != 'root': + msg = _("Only root certificate can be retrieved.") + raise webob.exc.HTTPNotImplemented(explanation=msg) + cert = self.cert_rpcapi.fetch_ca(context, + project_id=context.project_id) + return {'certificate': _translate_certificate_view(cert)} + + @wsgi.serializers(xml=CertificateTemplate) + def create(self, req, body=None): + """Create a certificate.""" + context = req.environ['nova.context'] + authorize(context) + pk, cert = self.cert_rpcapi.generate_x509_cert(context, + user_id=context.user_id, project_id=context.project_id) + context = req.environ['nova.context'] + return {'certificate': _translate_certificate_view(cert, pk)} + + +class Certificates(extensions.V3APIExtensionBase): + """Certificates support.""" + + name = "Certificates" + alias = ALIAS + namespace = ("http://docs.openstack.org/compute/ext/" + "certificates/api/v3") + version = 1 + + def get_resources(self): + resources = [ + extensions.ResourceExtension('os-certificates', + CertificatesController(), + member_actions={})] + return resources + + def get_controller_extensions(self): + return [] diff --git a/nova/api/openstack/compute/plugins/v3/evacuate.py b/nova/api/openstack/compute/plugins/v3/evacuate.py new file mode 100644 index 000000000..86e90e03e --- /dev/null +++ b/nova/api/openstack/compute/plugins/v3/evacuate.py @@ -0,0 +1,101 @@ +# Copyright 2013 OpenStack Foundation +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + + +from webob import exc + +from nova.api.openstack import common +from nova.api.openstack import extensions +from nova.api.openstack import wsgi +from nova import compute +from nova import exception +from nova.openstack.common import log as logging +from nova.openstack.common import strutils +from nova import utils + +LOG = logging.getLogger(__name__) +ALIAS = "os-evacuate" +authorize = extensions.extension_authorizer('compute', 'v3:' + ALIAS) + + +class EvacuateController(wsgi.Controller): + def __init__(self, *args, **kwargs): + super(EvacuateController, self).__init__(*args, **kwargs) + self.compute_api = compute.API() + + @wsgi.action('evacuate') + def _evacuate(self, req, id, body): + """ + Permit admins to evacuate a server from a failed host + to a new one. + """ + context = req.environ["nova.context"] + authorize(context) + + try: + if len(body) != 1: + raise exc.HTTPBadRequest(_("Malformed request body")) + + evacuate_body = body["evacuate"] + host = evacuate_body["host"] + on_shared_storage = strutils.bool_from_string( + evacuate_body["onSharedStorage"]) + + password = None + if 'adminPass' in evacuate_body: + # check that if requested to evacuate server on shared storage + # password not specified + if on_shared_storage: + msg = _("admin password can't be changed on existing disk") + raise exc.HTTPBadRequest(explanation=msg) + + password = evacuate_body['adminPass'] + elif not on_shared_storage: + password = utils.generate_password() + + except (TypeError, KeyError): + msg = _("host and onSharedStorage must be specified.") + raise exc.HTTPBadRequest(explanation=msg) + + try: + instance = self.compute_api.get(context, id) + self.compute_api.evacuate(context, instance, host, + on_shared_storage, password) + except exception.InstanceInvalidState as state_error: + common.raise_http_conflict_for_instance_invalid_state(state_error, + 'evacuate') + except Exception as e: + msg = _("Error in evacuate, %s") % e + LOG.exception(msg, instance=instance) + raise exc.HTTPBadRequest(explanation=msg) + + if password: + return {'adminPass': password} + + +class Evacuate(extensions.V3APIExtensionBase): + """Enables server evacuation.""" + + name = "Evacuate" + alias = ALIAS + namespace = "http://docs.openstack.org/compute/ext/evacuate/api/v3" + version = 1 + + def get_resources(self): + return [] + + def get_controller_extensions(self): + controller = EvacuateController() + extension = extensions.ControllerExtension(self, 'servers', controller) + return [extension] diff --git a/nova/api/openstack/compute/plugins/v3/extension_info.py b/nova/api/openstack/compute/plugins/v3/extension_info.py index 43b0551c7..c626f6104 100644 --- a/nova/api/openstack/compute/plugins/v3/extension_info.py +++ b/nova/api/openstack/compute/plugins/v3/extension_info.py @@ -19,6 +19,10 @@ import webob.exc from nova.api.openstack import extensions from nova.api.openstack import wsgi from nova.api.openstack import xmlutil +from nova.openstack.common import log as logging + + +LOG = logging.getLogger(__name__) def make_ext(elem): @@ -64,11 +68,25 @@ class ExtensionInfoController(object): ext_data['version'] = ext.version return ext_data + def _get_extensions(self, context): + """Filter extensions list based on policy""" + + discoverable_extensions = dict() + for alias, ext in self.extension_info.get_extensions().iteritems(): + authorize = extensions.soft_extension_authorizer( + 'compute', 'v3:' + alias) + if authorize(context, action='discoverable'): + discoverable_extensions[alias] = ext + else: + LOG.debug(_("Filter out extension %s from discover list"), alias) + return discoverable_extensions + @wsgi.serializers(xml=ExtensionsTemplate) def index(self, req): + context = req.environ['nova.context'] sorted_ext_list = sorted( - self.extension_info.get_extensions().iteritems()) + self._get_extensions(context).iteritems()) extensions = [] for _alias, ext in sorted_ext_list: @@ -77,9 +95,10 @@ class ExtensionInfoController(object): @wsgi.serializers(xml=ExtensionTemplate) def show(self, req, id): + context = req.environ['nova.context'] try: # NOTE(dprince): the extensions alias is used as the 'id' for show - ext = self.extension_info.get_extensions()[id] + ext = self._get_extensions(context)[id] except KeyError: raise webob.exc.HTTPNotFound() diff --git a/nova/api/openstack/compute/plugins/v3/fixed_ips.py b/nova/api/openstack/compute/plugins/v3/fixed_ips.py index e98b830bd..5fa4ae3c2 100644 --- a/nova/api/openstack/compute/plugins/v3/fixed_ips.py +++ b/nova/api/openstack/compute/plugins/v3/fixed_ips.py @@ -28,6 +28,7 @@ authorize = extensions.extension_authorizer('compute', 'v3:' + ALIAS) class FixedIPController(object): + @extensions.expected_errors(404) def show(self, req, id): """Return data about the given fixed ip.""" context = req.environ['nova.context'] @@ -55,6 +56,7 @@ class FixedIPController(object): return fixed_ip_info + @extensions.expected_errors((400, 404)) def action(self, req, id, body): context = req.environ['nova.context'] authorize(context) 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/keypairs.py b/nova/api/openstack/compute/plugins/v3/keypairs.py index bf740641e..ab40b051c 100644 --- a/nova/api/openstack/compute/plugins/v3/keypairs.py +++ b/nova/api/openstack/compute/plugins/v3/keypairs.py @@ -55,6 +55,7 @@ class KeypairController(object): self.api = compute_api.KeypairAPI() @wsgi.serializers(xml=KeypairTemplate) + @extensions.expected_errors((400, 409, 413)) def create(self, req, body): """ Create or import keypair. @@ -100,6 +101,7 @@ class KeypairController(object): except exception.KeyPairExists as exc: raise webob.exc.HTTPConflict(explanation=exc.format_message()) + @extensions.expected_errors(404) def delete(self, req, id): """ Delete a keypair with a given name @@ -113,6 +115,7 @@ class KeypairController(object): return webob.Response(status_int=202) @wsgi.serializers(xml=KeypairTemplate) + @extensions.expected_errors(404) def show(self, req, id): """Return data for the given key name.""" context = req.environ['nova.context'] 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/quota_sets.py b/nova/api/openstack/compute/plugins/v3/quota_sets.py new file mode 100644 index 000000000..67af5d127 --- /dev/null +++ b/nova/api/openstack/compute/plugins/v3/quota_sets.py @@ -0,0 +1,214 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# 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 + +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.openstack.common import log as logging +from nova.openstack.common import strutils +from nova import quota + + +ALIAS = "os-quota-sets" +QUOTAS = quota.QUOTAS +LOG = logging.getLogger(__name__) +NON_QUOTA_KEYS = ['tenant_id', 'id', 'force'] + + +authorize_update = extensions.extension_authorizer('compute', + 'v3:%s:update' % ALIAS) +authorize_show = extensions.extension_authorizer('compute', + 'v3:%s:show' % ALIAS) +authorize_delete = extensions.extension_authorizer('compute', + 'v3:%s:delete' % ALIAS) + + +class QuotaTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('quota_set', selector='quota_set') + root.set('id') + + for resource in QUOTAS.resources: + elem = xmlutil.SubTemplateElement(root, resource) + elem.text = resource + + return xmlutil.MasterTemplate(root, 1) + + +class QuotaSetsController(object): + + def __init__(self, ext_mgr): + self.ext_mgr = ext_mgr + + def _format_quota_set(self, project_id, quota_set): + """Convert the quota object to a result dict.""" + + result = dict(id=str(project_id)) + + for resource in QUOTAS.resources: + result[resource] = quota_set[resource] + + return dict(quota_set=result) + + def _validate_quota_limit(self, limit): + # NOTE: -1 is a flag value for unlimited + if limit < -1: + msg = _("Quota limit must be -1 or greater.") + raise webob.exc.HTTPBadRequest(explanation=msg) + + def _get_quotas(self, context, id, usages=False): + values = QUOTAS.get_project_quotas(context, id, usages=usages) + + if usages: + return values + else: + return dict((k, v['limit']) for k, v in values.items()) + + @wsgi.serializers(xml=QuotaTemplate) + def show(self, req, id): + context = req.environ['nova.context'] + authorize_show(context) + try: + nova.context.authorize_project_context(context, id) + return self._format_quota_set(id, self._get_quotas(context, id)) + except exception.NotAuthorized: + raise webob.exc.HTTPForbidden() + + @wsgi.serializers(xml=QuotaTemplate) + def update(self, req, id, body): + context = req.environ['nova.context'] + authorize_update(context) + project_id = id + + bad_keys = [] + + # By default, we can force update the quota if the extended + # is not loaded + force_update = True + extended_loaded = False + if self.ext_mgr.is_loaded('os-extended-quotas'): + # force optional has been enabled, the default value of + # force_update need to be changed to False + extended_loaded = True + force_update = False + + for key, value in body['quota_set'].items(): + if (key not in QUOTAS and + key not in NON_QUOTA_KEYS): + bad_keys.append(key) + continue + if key == 'force' and extended_loaded: + # only check the force optional when the extended has + # been loaded + force_update = strutils.bool_from_string(value) + elif key not in NON_QUOTA_KEYS and value: + try: + value = int(value) + except (ValueError, TypeError): + msg = _("Quota '%(value)s' for %(key)s should be " + "integer.") % locals() + LOG.warn(msg) + raise webob.exc.HTTPBadRequest(explanation=msg) + self._validate_quota_limit(value) + + LOG.debug(_("force update quotas: %s") % force_update) + + if len(bad_keys) > 0: + msg = _("Bad key(s) %s in quota_set") % ",".join(bad_keys) + raise webob.exc.HTTPBadRequest(explanation=msg) + + try: + project_quota = self._get_quotas(context, id, True) + except exception.NotAuthorized: + raise webob.exc.HTTPForbidden() + + for key, value in body['quota_set'].items(): + if key in NON_QUOTA_KEYS or not value: + continue + # validate whether already used and reserved exceeds the new + # quota, this check will be ignored if admin want to force + # update + value = int(value) + if force_update is not True and value >= 0: + quota_value = project_quota.get(key) + if quota_value and quota_value['limit'] >= 0: + quota_used = (quota_value['in_use'] + + quota_value['reserved']) + LOG.debug(_("Quota %(key)s used: %(quota_used)s, " + "value: %(value)s."), + {'key': key, 'quota_used': quota_used, + 'value': value}) + if quota_used > value: + msg = (_("Quota value %(value)s for %(key)s are " + "greater than already used and reserved " + "%(quota_used)s") % + {'value': value, 'key': key, + 'quota_used': quota_used}) + raise webob.exc.HTTPBadRequest(explanation=msg) + + try: + db.quota_update(context, project_id, key, value) + except exception.ProjectQuotaNotFound: + db.quota_create(context, project_id, key, value) + except exception.AdminRequired: + raise webob.exc.HTTPForbidden() + return {'quota_set': self._get_quotas(context, id)} + + @wsgi.serializers(xml=QuotaTemplate) + def defaults(self, req, id): + context = req.environ['nova.context'] + authorize_show(context) + return self._format_quota_set(id, QUOTAS.get_defaults(context)) + + def delete(self, req, id): + if self.ext_mgr.is_loaded('os-extended-quotas'): + context = req.environ['nova.context'] + authorize_delete(context) + try: + nova.context.authorize_project_context(context, id) + QUOTAS.destroy_all_by_project(context, id) + return webob.Response(status_int=202) + except exception.NotAuthorized: + raise webob.exc.HTTPForbidden() + raise webob.exc.HTTPNotFound() + + +class QuotaSets(extensions.V3APIExtensionBase): + """Quotas management support.""" + + name = "Quotas" + alias = ALIAS + namespace = "http://docs.openstack.org/compute/ext/os-quotas-sets/api/v3" + version = 1 + + def get_resources(self): + resources = [] + + res = extensions.ResourceExtension(ALIAS, + QuotaSetsController(self.ext_mgr), + member_actions={'defaults': 'GET'}) + resources.append(res) + + return resources + + def get_controller_extensions(self): + return [] diff --git a/nova/api/openstack/compute/plugins/v3/rescue.py b/nova/api/openstack/compute/plugins/v3/rescue.py new file mode 100644 index 000000000..ded18bb1a --- /dev/null +++ b/nova/api/openstack/compute/plugins/v3/rescue.py @@ -0,0 +1,100 @@ +# Copyright 2011 OpenStack Foundation +# +# 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 rescue mode extension.""" + +from oslo.config import cfg +import webob +from webob import exc + +from nova.api.openstack import common +from nova.api.openstack import extensions +from nova.api.openstack import wsgi +from nova import compute +from nova import exception +from nova import utils + + +ALIAS = "os-rescue" +CONF = cfg.CONF +authorize = extensions.extension_authorizer('compute', 'v3:' + ALIAS) + + +class RescueController(wsgi.Controller): + def __init__(self, *args, **kwargs): + super(RescueController, self).__init__(*args, **kwargs) + self.compute_api = compute.API() + + def _get_instance(self, context, instance_id): + try: + return self.compute_api.get(context, instance_id) + except exception.InstanceNotFound: + msg = _("Server not found") + raise exc.HTTPNotFound(msg) + + @wsgi.action('rescue') + def _rescue(self, req, id, body): + """Rescue an instance.""" + context = req.environ["nova.context"] + authorize(context) + + if body['rescue'] and 'adminPass' in body['rescue']: + password = body['rescue']['adminPass'] + else: + password = utils.generate_password() + + instance = self._get_instance(context, id) + try: + self.compute_api.rescue(context, instance, + rescue_password=password) + except exception.InstanceInvalidState as state_error: + common.raise_http_conflict_for_instance_invalid_state(state_error, + 'rescue') + except exception.InvalidVolume as volume_error: + raise exc.HTTPConflict(explanation=volume_error.format_message()) + except exception.InstanceNotRescuable as non_rescuable: + raise exc.HTTPBadRequest( + explanation=non_rescuable.format_message()) + + return {'adminPass': password} + + @wsgi.action('unrescue') + def _unrescue(self, req, id, body): + """Unrescue an instance.""" + context = req.environ["nova.context"] + authorize(context) + instance = self._get_instance(context, id) + try: + self.compute_api.unrescue(context, instance) + except exception.InstanceInvalidState as state_error: + common.raise_http_conflict_for_instance_invalid_state(state_error, + 'unrescue') + return webob.Response(status_int=202) + + +class Rescue(extensions.V3APIExtensionBase): + """Instance rescue mode.""" + + name = "Rescue" + alias = ALIAS + namespace = "http://docs.openstack.org/compute/ext/rescue/api/v3" + version = 1 + + def get_resources(self): + return [] + + def get_controller_extensions(self): + controller = RescueController() + extension = extensions.ControllerExtension(self, 'servers', controller) + return [extension] 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 [] |