diff options
Diffstat (limited to 'nova/api')
55 files changed, 2174 insertions, 170 deletions
diff --git a/nova/api/ec2/cloud.py b/nova/api/ec2/cloud.py index 47575b201..51a86e02f 100644 --- a/nova/api/ec2/cloud.py +++ b/nova/api/ec2/cloud.py @@ -211,9 +211,8 @@ def db_to_inst_obj(context, db_instance): # NOTE(danms): This is a temporary helper method for converting # Instance DB objects to NovaObjects without needing to re-query. inst_obj = instance_obj.Instance._from_db_object( - instance_obj.Instance(), db_instance, + context, instance_obj.Instance(), db_instance, expected_attrs=['system_metadata', 'metadata']) - inst_obj._context = context return inst_obj @@ -802,7 +801,6 @@ class CloudController(object): 'detaching': 'in-use'} instance_ec2_id = None - instance_data = None if volume.get('instance_uuid', None): instance_uuid = volume['instance_uuid'] @@ -810,8 +808,7 @@ class CloudController(object): instance_uuid) instance_ec2_id = ec2utils.id_to_ec2_inst_id(instance_uuid) - instance_data = '%s[%s]' % (instance_ec2_id, - instance['host']) + v = {} v['volumeId'] = ec2utils.id_to_ec2_vol_id(volume['id']) v['status'] = valid_ec2_api_volume_status_map.get(volume['status'], 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/metadata/password.py b/nova/api/metadata/password.py index f3453e945..793dcc0a7 100644 --- a/nova/api/metadata/password.py +++ b/nova/api/metadata/password.py @@ -27,10 +27,10 @@ MAX_SIZE = CHUNKS * CHUNK_LENGTH def extract_password(instance): result = '' - for datum in sorted(instance.get('system_metadata', []), - key=lambda x: x['key']): - if datum['key'].startswith('password_'): - result += datum['value'] + sys_meta = utils.instance_sys_meta(instance) + for key in sorted(sys_meta.keys()): + if key.startswith('password_'): + result += sys_meta[key] return result or None @@ -49,7 +49,6 @@ def convert_password(context, password): def handle_password(req, meta_data): ctxt = context.get_admin_context() - password = meta_data.password if req.method == 'GET': return meta_data.password elif req.method == 'POST': diff --git a/nova/api/openstack/common.py b/nova/api/openstack/common.py index bec919f4b..509256d87 100644 --- a/nova/api/openstack/common.py +++ b/nova/api/openstack/common.py @@ -358,9 +358,12 @@ def raise_http_conflict_for_instance_invalid_state(exc, action): """ attr = exc.kwargs.get('attr') state = exc.kwargs.get('state') + not_launched = exc.kwargs.get('not_launched') if attr and state: msg = _("Cannot '%(action)s' while instance is in %(attr)s " "%(state)s") % {'action': action, 'attr': attr, 'state': state} + elif not_launched: + msg = _("Cannot '%s' an instance which has never been active") % action else: # At least give some meaningful message msg = _("Instance is in an invalid state for '%s'") % action @@ -454,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", @@ -472,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)) @@ -480,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)) @@ -489,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/attach_interfaces.py b/nova/api/openstack/compute/contrib/attach_interfaces.py index ec565a0d1..a823eed2b 100644 --- a/nova/api/openstack/compute/contrib/attach_interfaces.py +++ b/nova/api/openstack/compute/contrib/attach_interfaces.py @@ -60,7 +60,7 @@ class InterfaceAttachmentController(object): port_id = id try: - instance = self.compute_api.get(context, server_id) + self.compute_api.get(context, server_id) except exception.NotFound: raise exc.HTTPNotFound() diff --git a/nova/api/openstack/compute/contrib/availability_zone.py b/nova/api/openstack/compute/contrib/availability_zone.py index 22001b65f..1f22bf252 100644 --- a/nova/api/openstack/compute/contrib/availability_zone.py +++ b/nova/api/openstack/compute/contrib/availability_zone.py @@ -12,7 +12,7 @@ # 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 +# under the License. from oslo.config import cfg diff --git a/nova/api/openstack/compute/contrib/cell_capacities.py b/nova/api/openstack/compute/contrib/cell_capacities.py index ae8b42336..ab79b4327 100644 --- a/nova/api/openstack/compute/contrib/cell_capacities.py +++ b/nova/api/openstack/compute/contrib/cell_capacities.py @@ -12,7 +12,7 @@ # 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 +# under the License. from nova.api.openstack import extensions diff --git a/nova/api/openstack/compute/contrib/certificates.py b/nova/api/openstack/compute/contrib/certificates.py index 64a6e26fe..979008a87 100644 --- a/nova/api/openstack/compute/contrib/certificates.py +++ b/nova/api/openstack/compute/contrib/certificates.py @@ -12,7 +12,7 @@ # 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 +# under the License. import webob.exc @@ -38,15 +38,6 @@ class CertificateTemplate(xmlutil.TemplateBuilder): return xmlutil.MasterTemplate(root, 1) -class CertificatesTemplate(xmlutil.TemplateBuilder): - def construct(self): - root = xmlutil.TemplateElement('certificates') - elem = xmlutil.SubTemplateElement(root, 'certificate', - selector='certificates') - make_certificate(elem) - return xmlutil.MasterTemplate(root, 1) - - def _translate_certificate_view(certificate, private_key=None): return { 'data': certificate, @@ -64,7 +55,7 @@ class CertificatesController(object): @wsgi.serializers(xml=CertificateTemplate) def show(self, req, id): - """Return a list of certificates.""" + """Return certificate information.""" context = req.environ['nova.context'] authorize(context) if id != 'root': @@ -76,7 +67,7 @@ class CertificatesController(object): @wsgi.serializers(xml=CertificateTemplate) def create(self, req, body=None): - """Return a list of certificates.""" + """Create a certificate.""" context = req.environ['nova.context'] authorize(context) pk, cert = self.cert_rpcapi.generate_x509_cert(context, diff --git a/nova/api/openstack/compute/contrib/console_output.py b/nova/api/openstack/compute/contrib/console_output.py index eb20b3275..07b3f2556 100644 --- a/nova/api/openstack/compute/contrib/console_output.py +++ b/nova/api/openstack/compute/contrib/console_output.py @@ -14,7 +14,7 @@ # 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 +# under the License. import re import webob diff --git a/nova/api/openstack/compute/contrib/consoles.py b/nova/api/openstack/compute/contrib/consoles.py index bf1f41690..8247620f2 100644 --- a/nova/api/openstack/compute/contrib/consoles.py +++ b/nova/api/openstack/compute/contrib/consoles.py @@ -12,7 +12,7 @@ # 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 +# under the License. import webob diff --git a/nova/api/openstack/compute/contrib/coverage_ext.py b/nova/api/openstack/compute/contrib/coverage_ext.py index 154699470..fd1ad53e9 100644 --- a/nova/api/openstack/compute/contrib/coverage_ext.py +++ b/nova/api/openstack/compute/contrib/coverage_ext.py @@ -12,7 +12,7 @@ # 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 +# under the License. # See: http://wiki.openstack.org/Nova/CoverageExtension for more information # and usage explanation for this API extension @@ -134,11 +134,12 @@ class CoverageController(object): # doesn't resolve to 127.0.0.1. Currently backdoors only open on # loopback so this is for covering the common single host use case except socket.error as e: + exc_info = sys.exc_info() if 'ECONNREFUSED' in e and service['host'] == self.host: service['telnet'] = telnetlib.Telnet('127.0.0.1', service['port']) else: - raise e + raise exc_info[0], exc_info[1], exc_info[2] self.services.append(service) self._start_coverage_telnet(service['telnet'], service['service']) diff --git a/nova/api/openstack/compute/contrib/createserverext.py b/nova/api/openstack/compute/contrib/createserverext.py index 337fedae6..9559ceef3 100644 --- a/nova/api/openstack/compute/contrib/createserverext.py +++ b/nova/api/openstack/compute/contrib/createserverext.py @@ -12,7 +12,7 @@ # 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 +# under the License. from nova.api.openstack import extensions diff --git a/nova/api/openstack/compute/contrib/disk_config.py b/nova/api/openstack/compute/contrib/disk_config.py index f2b906144..692ed6455 100644 --- a/nova/api/openstack/compute/contrib/disk_config.py +++ b/nova/api/openstack/compute/contrib/disk_config.py @@ -12,7 +12,7 @@ # 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 +# under the License. """Disk Config extension.""" 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/contrib/extended_floating_ips.py b/nova/api/openstack/compute/contrib/extended_floating_ips.py index 06f1fa903..44b84bad9 100644 --- a/nova/api/openstack/compute/contrib/extended_floating_ips.py +++ b/nova/api/openstack/compute/contrib/extended_floating_ips.py @@ -12,7 +12,7 @@ # 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 +# under the License. from nova.api.openstack import extensions diff --git a/nova/api/openstack/compute/contrib/extended_ips.py b/nova/api/openstack/compute/contrib/extended_ips.py index ac75293a6..20356c08d 100644 --- a/nova/api/openstack/compute/contrib/extended_ips.py +++ b/nova/api/openstack/compute/contrib/extended_ips.py @@ -94,7 +94,7 @@ def make_server(elem): class ExtendedIpsServerTemplate(xmlutil.TemplateBuilder): def construct(self): root = xmlutil.TemplateElement('server', selector='server') - elem = xmlutil.SubTemplateElement(root, 'server', selector='servers') + xmlutil.SubTemplateElement(root, 'server', selector='servers') make_server(root) return xmlutil.SlaveTemplate(root, 1, nsmap={ Extended_ips.alias: Extended_ips.namespace}) diff --git a/nova/api/openstack/compute/contrib/fixed_ips.py b/nova/api/openstack/compute/contrib/fixed_ips.py index b474b685d..532282a2b 100644 --- a/nova/api/openstack/compute/contrib/fixed_ips.py +++ b/nova/api/openstack/compute/contrib/fixed_ips.py @@ -69,8 +69,7 @@ class FixedIPController(object): fixed_ip = db.fixed_ip_get_by_address(context, address) db.fixed_ip_update(context, fixed_ip['address'], {'reserved': reserved}) - except (exception.FixedIpNotFoundForAddress, - exception.FixedIpInvalid) as ex: + except (exception.FixedIpNotFoundForAddress, exception.FixedIpInvalid): msg = _("Fixed IP %s not found") % address raise webob.exc.HTTPNotFound(explanation=msg) diff --git a/nova/api/openstack/compute/contrib/flavorextraspecs.py b/nova/api/openstack/compute/contrib/flavorextraspecs.py index 6e33d3603..00e7d7233 100644 --- a/nova/api/openstack/compute/contrib/flavorextraspecs.py +++ b/nova/api/openstack/compute/contrib/flavorextraspecs.py @@ -104,7 +104,7 @@ class FlavorExtraSpecsController(object): extra_spec = db.instance_type_extra_specs_get_item(context, flavor_id, id) return extra_spec - except exception.InstanceTypeExtraSpecsNotFound as e: + except exception.InstanceTypeExtraSpecsNotFound: raise exc.HTTPNotFound() def delete(self, req, flavor_id, id): diff --git a/nova/api/openstack/compute/contrib/flavormanage.py b/nova/api/openstack/compute/contrib/flavormanage.py index 43d5d2110..602e82c36 100644 --- a/nova/api/openstack/compute/contrib/flavormanage.py +++ b/nova/api/openstack/compute/contrib/flavormanage.py @@ -10,7 +10,7 @@ # 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 +# under the License. import webob @@ -76,6 +76,8 @@ class FlavorManageController(wsgi.Controller): except (exception.InstanceTypeExists, exception.InstanceTypeIdExists) as err: raise webob.exc.HTTPConflict(explanation=err.format_message()) + except exception.InvalidInput as exc: + raise webob.exc.HTTPBadRequest(explanation=exc.format_message()) return self._view_builder.show(req, flavor) diff --git a/nova/api/openstack/compute/contrib/floating_ip_dns.py b/nova/api/openstack/compute/contrib/floating_ip_dns.py index ecaa8e7b9..1d6a8b812 100644 --- a/nova/api/openstack/compute/contrib/floating_ip_dns.py +++ b/nova/api/openstack/compute/contrib/floating_ip_dns.py @@ -12,7 +12,7 @@ # 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 +# under the License. import urllib diff --git a/nova/api/openstack/compute/contrib/floating_ip_pools.py b/nova/api/openstack/compute/contrib/floating_ip_pools.py index e792ce433..aefe65d26 100644 --- a/nova/api/openstack/compute/contrib/floating_ip_pools.py +++ b/nova/api/openstack/compute/contrib/floating_ip_pools.py @@ -12,7 +12,7 @@ # 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 +# under the License. from nova.api.openstack import extensions from nova.api.openstack import wsgi diff --git a/nova/api/openstack/compute/contrib/floating_ips.py b/nova/api/openstack/compute/contrib/floating_ips.py index 284a211cd..807201e7e 100644 --- a/nova/api/openstack/compute/contrib/floating_ips.py +++ b/nova/api/openstack/compute/contrib/floating_ips.py @@ -15,7 +15,7 @@ # 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 +# under the License. import webob diff --git a/nova/api/openstack/compute/contrib/multiple_create.py b/nova/api/openstack/compute/contrib/multiple_create.py index fd450b6d8..ee3dcf4b4 100644 --- a/nova/api/openstack/compute/contrib/multiple_create.py +++ b/nova/api/openstack/compute/contrib/multiple_create.py @@ -12,7 +12,7 @@ # 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 +# under the License. from nova.api.openstack import extensions diff --git a/nova/api/openstack/compute/contrib/scheduler_hints.py b/nova/api/openstack/compute/contrib/scheduler_hints.py index 0775307f4..0a5136205 100644 --- a/nova/api/openstack/compute/contrib/scheduler_hints.py +++ b/nova/api/openstack/compute/contrib/scheduler_hints.py @@ -12,7 +12,7 @@ # 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 +# under the License. import webob.exc diff --git a/nova/api/openstack/compute/contrib/security_groups.py b/nova/api/openstack/compute/contrib/security_groups.py index b34a77cab..e2862be4e 100644 --- a/nova/api/openstack/compute/contrib/security_groups.py +++ b/nova/api/openstack/compute/contrib/security_groups.py @@ -16,6 +16,7 @@ """The security groups extension.""" +import contextlib import json import webob from webob import exc @@ -177,6 +178,25 @@ class SecurityGroupRulesXMLDeserializer(wsgi.MetadataXMLDeserializer): return sg_rule +@contextlib.contextmanager +def translate_exceptions(): + """Translate nova exceptions to http exceptions.""" + try: + yield + except exception.Invalid as exp: + msg = exp.format_message() + raise exc.HTTPBadRequest(explanation=msg) + except exception.SecurityGroupNotFound as exp: + msg = exp.format_message() + raise exc.HTTPNotFound(explanation=msg) + except exception.InstanceNotFound as exp: + msg = exp.format_message() + raise exc.HTTPNotFound(explanation=msg) + except exception.SecurityGroupLimitExceeded as exp: + msg = exp.format_message() + raise exc.HTTPRequestEntityTooLarge(explanation=msg) + + class SecurityGroupControllerBase(object): """Base class for Security Group controllers.""" @@ -196,8 +216,9 @@ class SecurityGroupControllerBase(object): sg_rule['group'] = {} sg_rule['ip_range'] = {} if rule['group_id']: - source_group = self.security_group_api.get(context, - id=rule['group_id']) + with translate_exceptions(): + source_group = self.security_group_api.get(context, + id=rule['group_id']) sg_rule['group'] = {'name': source_group.get('name'), 'tenant_id': source_group.get('project_id')} else: @@ -233,10 +254,10 @@ class SecurityGroupController(SecurityGroupControllerBase): """Return data about the given security group.""" context = _authorize_context(req) - id = self.security_group_api.validate_id(id) - - security_group = self.security_group_api.get(context, None, id, - map_exception=True) + with translate_exceptions(): + id = self.security_group_api.validate_id(id) + security_group = self.security_group_api.get(context, None, id, + map_exception=True) return {'security_group': self._format_security_group(context, security_group)} @@ -245,12 +266,11 @@ class SecurityGroupController(SecurityGroupControllerBase): """Delete a security group.""" context = _authorize_context(req) - id = self.security_group_api.validate_id(id) - - security_group = self.security_group_api.get(context, None, id, - map_exception=True) - - self.security_group_api.destroy(context, security_group) + with translate_exceptions(): + id = self.security_group_api.validate_id(id) + security_group = self.security_group_api.get(context, None, id, + map_exception=True) + self.security_group_api.destroy(context, security_group) return webob.Response(status_int=202) @@ -262,9 +282,11 @@ class SecurityGroupController(SecurityGroupControllerBase): search_opts = {} search_opts.update(req.GET) - raw_groups = self.security_group_api.list(context, - project=context.project_id, - search_opts=search_opts) + with translate_exceptions(): + project_id = context.project_id + raw_groups = self.security_group_api.list(context, + project=project_id, + search_opts=search_opts) limited_list = common.limited(raw_groups, req) result = [self._format_security_group(context, group) @@ -285,16 +307,12 @@ class SecurityGroupController(SecurityGroupControllerBase): group_name = security_group.get('name', None) group_description = security_group.get('description', None) - self.security_group_api.validate_property(group_name, 'name', None) - self.security_group_api.validate_property(group_description, - 'description', None) - - try: + with translate_exceptions(): + self.security_group_api.validate_property(group_name, 'name', None) + self.security_group_api.validate_property(group_description, + 'description', None) group_ref = self.security_group_api.create_security_group( context, group_name, group_description) - except exception.SecurityGroupLimitExceeded as err: - raise exc.HTTPRequestEntityTooLarge( - explanation=err.format_message()) return {'security_group': self._format_security_group(context, group_ref)} @@ -304,21 +322,21 @@ class SecurityGroupController(SecurityGroupControllerBase): """Update a security group.""" context = _authorize_context(req) - id = self.security_group_api.validate_id(id) + with translate_exceptions(): + id = self.security_group_api.validate_id(id) + security_group = self.security_group_api.get(context, None, id, + map_exception=True) - security_group = self.security_group_api.get(context, None, id, - map_exception=True) security_group_data = self._from_body(body, 'security_group') - group_name = security_group_data.get('name', None) group_description = security_group_data.get('description', None) - self.security_group_api.validate_property(group_name, 'name', None) - self.security_group_api.validate_property(group_description, - 'description', None) - - group_ref = self.security_group_api.update_security_group( - context, security_group, group_name, group_description) + with translate_exceptions(): + self.security_group_api.validate_property(group_name, 'name', None) + self.security_group_api.validate_property(group_description, + 'description', None) + group_ref = self.security_group_api.update_security_group( + context, security_group, group_name, group_description) return {'security_group': self._format_security_group(context, group_ref)} @@ -333,11 +351,12 @@ class SecurityGroupRulesController(SecurityGroupControllerBase): sg_rule = self._from_body(body, 'security_group_rule') - parent_group_id = self.security_group_api.validate_id( - sg_rule.get('parent_group_id', None)) - - security_group = self.security_group_api.get(context, None, - parent_group_id, map_exception=True) + with translate_exceptions(): + parent_group_id = self.security_group_api.validate_id( + sg_rule.get('parent_group_id', None)) + security_group = self.security_group_api.get(context, None, + parent_group_id, + map_exception=True) try: new_rule = self._rule_args_to_dict(context, to_port=sg_rule.get('to_port'), @@ -360,13 +379,10 @@ class SecurityGroupRulesController(SecurityGroupControllerBase): msg = _("Bad prefix for network in cidr %s") % new_rule['cidr'] raise exc.HTTPBadRequest(explanation=msg) - try: + with translate_exceptions(): security_group_rule = ( self.security_group_api.create_security_group_rule( context, security_group, new_rule)) - except exception.SecurityGroupLimitExceeded as err: - raise exc.HTTPRequestEntityTooLarge( - explanation=err.format_message()) return {"security_group_rule": self._format_security_group_rule( context, @@ -390,17 +406,15 @@ class SecurityGroupRulesController(SecurityGroupControllerBase): def delete(self, req, id): context = _authorize_context(req) - id = self.security_group_api.validate_id(id) - - rule = self.security_group_api.get_rule(context, id) - - group_id = rule['parent_group_id'] - - security_group = self.security_group_api.get(context, None, group_id, - map_exception=True) - - self.security_group_api.remove_rules(context, security_group, - [rule['id']]) + with translate_exceptions(): + id = self.security_group_api.validate_id(id) + rule = self.security_group_api.get_rule(context, id) + group_id = rule['parent_group_id'] + security_group = self.security_group_api.get(context, None, + group_id, + map_exception=True) + self.security_group_api.remove_rules(context, security_group, + [rule['id']]) return webob.Response(status_int=202) @@ -414,13 +428,11 @@ class ServerSecurityGroupController(SecurityGroupControllerBase): self.security_group_api.ensure_default(context) - try: + with translate_exceptions(): instance = self.compute_api.get(context, server_id) - except exception.InstanceNotFound as exp: - raise exc.HTTPNotFound(explanation=exp.format_message()) + groups = self.security_group_api.get_instance_security_groups( + context, instance['uuid'], True) - groups = self.security_group_api.get_instance_security_groups( - context, instance['uuid'], True) result = [self._format_security_group(context, group) for group in groups] @@ -455,15 +467,9 @@ class SecurityGroupActionController(wsgi.Controller): return group_name def _invoke(self, method, context, id, group_name): - try: + with translate_exceptions(): instance = self.compute_api.get(context, id) method(context, instance, group_name) - except exception.SecurityGroupNotFound as exp: - raise exc.HTTPNotFound(explanation=exp.format_message()) - except exception.InstanceNotFound as exp: - raise exc.HTTPNotFound(explanation=exp.format_message()) - except exception.Invalid as exp: - raise exc.HTTPBadRequest(explanation=exp.format_message()) return webob.Response(status_int=202) @@ -642,15 +648,15 @@ class Security_groups(extensions.ExtensionDescriptor): class NativeSecurityGroupExceptions(object): @staticmethod def raise_invalid_property(msg): - raise exc.HTTPBadRequest(explanation=msg) + raise exception.Invalid(msg) @staticmethod def raise_group_already_exists(msg): - raise exc.HTTPBadRequest(explanation=msg) + raise exception.Invalid(msg) @staticmethod def raise_invalid_group(msg): - raise exc.HTTPBadRequest(explanation=msg) + raise exception.Invalid(msg) @staticmethod def raise_invalid_cidr(cidr, decoding_exception=None): @@ -662,7 +668,7 @@ class NativeSecurityGroupExceptions(object): @staticmethod def raise_not_found(msg): - raise exc.HTTPNotFound(explanation=msg) + raise exception.SecurityGroupNotFound(msg) class NativeNovaSecurityGroupAPI(NativeSecurityGroupExceptions, diff --git a/nova/api/openstack/compute/contrib/server_password.py b/nova/api/openstack/compute/contrib/server_password.py index 14ea91ef2..2884e4040 100644 --- a/nova/api/openstack/compute/contrib/server_password.py +++ b/nova/api/openstack/compute/contrib/server_password.py @@ -39,7 +39,7 @@ class ServerPasswordTemplate(xmlutil.TemplateBuilder): class ServerPasswordController(object): - """The flavor access API controller for the OpenStack API.""" + """The Server Password API controller for the OpenStack API.""" def __init__(self): self.compute_api = compute.API() diff --git a/nova/api/openstack/compute/contrib/server_start_stop.py b/nova/api/openstack/compute/contrib/server_start_stop.py index 2803cd04b..1734b2cfa 100644 --- a/nova/api/openstack/compute/contrib/server_start_stop.py +++ b/nova/api/openstack/compute/contrib/server_start_stop.py @@ -12,7 +12,7 @@ # 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 +# under the License. import webob diff --git a/nova/api/openstack/compute/contrib/services.py b/nova/api/openstack/compute/contrib/services.py index 3a637010a..76eec7ce4 100644 --- a/nova/api/openstack/compute/contrib/services.py +++ b/nova/api/openstack/compute/contrib/services.py @@ -183,8 +183,7 @@ class ServiceController(object): raise webob.exc.HTTPUnprocessableEntity(detail=msg) try: - svc = self.host_api.service_update(context, host, binary, - status_detail) + self.host_api.service_update(context, host, binary, status_detail) except exception.ServiceNotFound: raise webob.exc.HTTPNotFound(_("Unknown service")) diff --git a/nova/api/openstack/compute/contrib/used_limits.py b/nova/api/openstack/compute/contrib/used_limits.py index 5a90a9def..a0fab1594 100644 --- a/nova/api/openstack/compute/contrib/used_limits.py +++ b/nova/api/openstack/compute/contrib/used_limits.py @@ -12,7 +12,7 @@ # 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 +# under the License. from nova.api.openstack import extensions from nova.api.openstack import wsgi diff --git a/nova/api/openstack/compute/contrib/used_limits_for_admin.py b/nova/api/openstack/compute/contrib/used_limits_for_admin.py index a6ec9c002..d2a8af0d7 100644 --- a/nova/api/openstack/compute/contrib/used_limits_for_admin.py +++ b/nova/api/openstack/compute/contrib/used_limits_for_admin.py @@ -12,7 +12,7 @@ # 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 +# under the License. from nova.api.openstack import extensions diff --git a/nova/api/openstack/compute/contrib/user_data.py b/nova/api/openstack/compute/contrib/user_data.py index 3e69a65cc..ef5aa6d5a 100644 --- a/nova/api/openstack/compute/contrib/user_data.py +++ b/nova/api/openstack/compute/contrib/user_data.py @@ -12,7 +12,7 @@ # 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 +# under the License. from nova.api.openstack import extensions diff --git a/nova/api/openstack/compute/extensions.py b/nova/api/openstack/compute/extensions.py index 5e6633f1d..ac8c04aa0 100644 --- a/nova/api/openstack/compute/extensions.py +++ b/nova/api/openstack/compute/extensions.py @@ -19,7 +19,6 @@ from oslo.config import cfg from nova.api.openstack import extensions as base_extensions from nova.openstack.common import log as logging -from nova.openstack.common.plugin import pluginmanager ext_opts = [ cfg.MultiStrOpt('osapi_compute_extension', @@ -38,10 +37,6 @@ class ExtensionManager(base_extensions.ExtensionManager): def __init__(self): LOG.audit(_('Initializing extension manager.')) self.cls_list = CONF.osapi_compute_extension - self.PluginManager = pluginmanager.PluginManager('nova', - 'compute-extensions') - self.PluginManager.load_plugins() - self.cls_list.append(self.PluginManager.plugin_extension_factory) self.extensions = {} self.sorted_ext_list = [] self._load_extensions() 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/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 [] 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 de7a51812..8c329d12b 100644 --- a/nova/api/openstack/compute/servers.py +++ b/nova/api/openstack/compute/servers.py @@ -552,10 +552,10 @@ class Controller(wsgi.Controller): search_opts=search_opts, limit=limit, marker=marker) - except exception.MarkerNotFound as e: + except exception.MarkerNotFound: msg = _('marker [%s] not found') % marker raise exc.HTTPBadRequest(explanation=msg) - except exception.FlavorNotFound as e: + except exception.FlavorNotFound: log_msg = _("Flavor '%s' could not be found ") LOG.debug(log_msg, search_opts['flavor']) instance_list = [] @@ -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} @@ -1093,11 +1096,11 @@ class Controller(wsgi.Controller): except exception.InstanceInvalidState as state_error: common.raise_http_conflict_for_instance_invalid_state(state_error, 'resize') - except exception.ImageNotAuthorized as image_error: + except exception.ImageNotAuthorized: msg = _("You are not authorized to access the image " "the instance was started with.") raise exc.HTTPUnauthorized(explanation=msg) - except exception.ImageNotFound as image_error: + except exception.ImageNotFound: msg = _("Image that the instance was started " "with could not be found.") raise exc.HTTPBadRequest(explanation=msg) 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/extensions.py b/nova/api/openstack/extensions.py index 6cbc5bb78..69cc87546 100644 --- a/nova/api/openstack/extensions.py +++ b/nova/api/openstack/extensions.py @@ -17,6 +17,7 @@ # under the License. import abc +import functools import os import webob.dec @@ -392,9 +393,9 @@ def extension_authorizer(api_name, extension_name): def soft_extension_authorizer(api_name, extension_name): hard_authorize = extension_authorizer(api_name, extension_name) - def authorize(context): + def authorize(context, action=None): try: - hard_authorize(context) + hard_authorize(context, action=action) return True except exception.NotAuthorized: return False @@ -451,3 +452,42 @@ class V3APIExtensionBase(object): def version(self): """Version of the extension.""" pass + + +def expected_errors(errors): + """Decorator for v3 API methods which specifies expected exceptions. + + Specify which exceptions may occur when an API method is called. If an + unexpected exception occurs then return a 500 instead and ask the user + of the API to file a bug report. + """ + def decorator(f): + @functools.wraps(f) + def wrapped(*args, **kwargs): + try: + return f(*args, **kwargs) + except Exception as exc: + if isinstance(exc, webob.exc.WSGIHTTPException): + if isinstance(errors, int): + t_errors = (errors,) + else: + t_errors = errors + if exc.code in t_errors: + raise + elif isinstance(exc, exception.PolicyNotAuthorized): + # Note(cyeoh): Special case to handle + # PolicyNotAuthorized exceptions so every + # extension method does not need to wrap authorize + # calls. ResourceExceptionHandler silently + # converts NotAuthorized to HTTPForbidden + raise + + LOG.exception(_("Unexpected exception in API method")) + msg = _('Unexpected API Error. Please report this at ' + 'http://bugs.launchpad.net/nova/ and attach the Nova ' + 'API log if possible.\n%s') % type(exc) + raise webob.exc.HTTPInternalServerError(explanation=msg) + + return wrapped + + return decorator 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) |