diff options
| author | Tushar Patil <tushar.vitthal.patil@gmail.com> | 2011-08-09 16:26:12 -0700 |
|---|---|---|
| committer | Tushar Patil <tushar.vitthal.patil@gmail.com> | 2011-08-09 16:26:12 -0700 |
| commit | 8a8b71b2eaf72b03c0c2bc847b449d2d640fc6c0 (patch) | |
| tree | b5be7eacff26e098b93eff60b90e57a25160cb6c /nova/api | |
| parent | 96631a9e1188d1781381cafc409c2ec3ead895fb (diff) | |
| parent | 4b3165429797d40da17f5c59aaeadb00673b71b2 (diff) | |
| download | nova-8a8b71b2eaf72b03c0c2bc847b449d2d640fc6c0.tar.gz nova-8a8b71b2eaf72b03c0c2bc847b449d2d640fc6c0.tar.xz nova-8a8b71b2eaf72b03c0c2bc847b449d2d640fc6c0.zip | |
Merged with trunk
Diffstat (limited to 'nova/api')
| -rw-r--r-- | nova/api/ec2/__init__.py | 4 | ||||
| -rw-r--r-- | nova/api/ec2/cloud.py | 243 | ||||
| -rw-r--r-- | nova/api/ec2/ec2utils.py | 29 | ||||
| -rw-r--r-- | nova/api/openstack/common.py | 46 | ||||
| -rw-r--r-- | nova/api/openstack/contrib/admin_only.py | 30 | ||||
| -rw-r--r-- | nova/api/openstack/contrib/floating_ips.py | 12 | ||||
| -rw-r--r-- | nova/api/openstack/contrib/hosts.py | 32 | ||||
| -rw-r--r-- | nova/api/openstack/extensions.py | 31 | ||||
| -rw-r--r-- | nova/api/openstack/image_metadata.py | 16 | ||||
| -rw-r--r-- | nova/api/openstack/schemas/atom-link.rng | 141 | ||||
| -rw-r--r-- | nova/api/openstack/schemas/atom.rng | 597 | ||||
| -rw-r--r-- | nova/api/openstack/schemas/v1.1/extension.rng | 11 | ||||
| -rw-r--r-- | nova/api/openstack/schemas/v1.1/extensions.rng | 6 | ||||
| -rw-r--r-- | nova/api/openstack/server_metadata.py | 51 | ||||
| -rw-r--r-- | nova/api/openstack/servers.py | 106 | ||||
| -rw-r--r-- | nova/api/openstack/views/images.py | 4 | ||||
| -rw-r--r-- | nova/api/openstack/views/servers.py | 16 | ||||
| -rw-r--r-- | nova/api/openstack/xmlutil.py | 37 | ||||
| -rw-r--r-- | nova/api/openstack/zones.py | 2 |
19 files changed, 1252 insertions, 162 deletions
diff --git a/nova/api/ec2/__init__.py b/nova/api/ec2/__init__.py index 804e54ef9..8b6e47cfb 100644 --- a/nova/api/ec2/__init__.py +++ b/nova/api/ec2/__init__.py @@ -354,6 +354,10 @@ class Executor(wsgi.Application): LOG.debug(_('KeyPairExists raised: %s'), unicode(ex), context=context) return self._error(req, context, type(ex).__name__, unicode(ex)) + except exception.InvalidParameterValue as ex: + LOG.debug(_('InvalidParameterValue raised: %s'), unicode(ex), + context=context) + return self._error(req, context, type(ex).__name__, unicode(ex)) except Exception as ex: extra = {'environment': req.environ} LOG.exception(_('Unexpected error raised: %s'), unicode(ex), diff --git a/nova/api/ec2/cloud.py b/nova/api/ec2/cloud.py index 0294c09c5..87bba58c3 100644 --- a/nova/api/ec2/cloud.py +++ b/nova/api/ec2/cloud.py @@ -25,11 +25,13 @@ datastore. import base64 import netaddr import os -import urllib +import re +import shutil import tempfile import time -import shutil +import urllib +from nova import block_device from nova import compute from nova import context @@ -78,6 +80,10 @@ def _gen_key(context, user_id, key_name): # TODO(yamahata): hypervisor dependent default device name _DEFAULT_ROOT_DEVICE_NAME = '/dev/sda1' +_DEFAULT_MAPPINGS = {'ami': 'sda1', + 'ephemeral0': 'sda2', + 'root': _DEFAULT_ROOT_DEVICE_NAME, + 'swap': 'sda3'} def _parse_block_device_mapping(bdm): @@ -105,7 +111,7 @@ def _parse_block_device_mapping(bdm): def _properties_get_mappings(properties): - return ec2utils.mappings_prepend_dev(properties.get('mappings', [])) + return block_device.mappings_prepend_dev(properties.get('mappings', [])) def _format_block_device_mapping(bdm): @@ -144,8 +150,7 @@ def _format_mappings(properties, result): """Format multiple BlockDeviceMappingItemType""" mappings = [{'virtualName': m['virtual'], 'deviceName': m['device']} for m in _properties_get_mappings(properties) - if (m['virtual'] == 'swap' or - m['virtual'].startswith('ephemeral'))] + if block_device.is_swap_or_ephemeral(m['virtual'])] block_device_mapping = [_format_block_device_mapping(bdm) for bdm in properties.get('block_device_mapping', [])] @@ -208,8 +213,9 @@ class CloudController(object): def _get_mpi_data(self, context, project_id): result = {} + search_opts = {'project_id': project_id} for instance in self.compute_api.get_all(context, - project_id=project_id): + search_opts=search_opts): if instance['fixed_ips']: line = '%s slots=%d' % (instance['fixed_ips'][0]['address'], instance['vcpus']) @@ -233,10 +239,39 @@ class CloudController(object): state = 'available' return image['properties'].get('image_state', state) + def _format_instance_mapping(self, ctxt, instance_ref): + root_device_name = instance_ref['root_device_name'] + if root_device_name is None: + return _DEFAULT_MAPPINGS + + mappings = {} + mappings['ami'] = block_device.strip_dev(root_device_name) + mappings['root'] = root_device_name + + # 'ephemeralN' and 'swap' + for bdm in db.block_device_mapping_get_all_by_instance( + ctxt, instance_ref['id']): + if (bdm['volume_id'] or bdm['snapshot_id'] or bdm['no_device']): + continue + + virtual_name = bdm['virtual_name'] + if not virtual_name: + continue + + if block_device.is_swap_or_ephemeral(virtual_name): + mappings[virtual_name] = bdm['device_name'] + + return mappings + def get_metadata(self, address): ctxt = context.get_admin_context() - instance_ref = self.compute_api.get_all(ctxt, fixed_ip=address) - if instance_ref is None: + search_opts = {'fixed_ip': address} + try: + instance_ref = self.compute_api.get_all(ctxt, + search_opts=search_opts) + except exception.NotFound: + instance_ref = None + if not instance_ref: return None # This ensures that all attributes of the instance @@ -259,18 +294,14 @@ class CloudController(object): security_groups = db.security_group_get_by_instance(ctxt, instance_ref['id']) security_groups = [x['name'] for x in security_groups] + mappings = self._format_instance_mapping(ctxt, instance_ref) data = { - 'user-data': base64.b64decode(instance_ref['user_data']), + 'user-data': self._format_user_data(instance_ref), 'meta-data': { 'ami-id': image_ec2_id, 'ami-launch-index': instance_ref['launch_index'], 'ami-manifest-path': 'FIXME', - 'block-device-mapping': { - # TODO(vish): replace with real data - 'ami': 'sda1', - 'ephemeral0': 'sda2', - 'root': _DEFAULT_ROOT_DEVICE_NAME, - 'swap': 'sda3'}, + 'block-device-mapping': mappings, 'hostname': hostname, 'instance-action': 'none', 'instance-id': ec2_id, @@ -765,6 +796,22 @@ class CloudController(object): return source_project_id def create_security_group(self, context, group_name, group_description): + if not re.match('^[a-zA-Z0-9_\- ]+$', str(group_name)): + # Some validation to ensure that values match API spec. + # - Alphanumeric characters, spaces, dashes, and underscores. + # TODO(Daviey): LP: #813685 extend beyond group_name checking, and + # probably create a param validator that can be used elsewhere. + err = _("Value (%s) for parameter GroupName is invalid." + " Content limited to Alphanumeric characters, " + "spaces, dashes, and underscores.") % group_name + # err not that of master ec2 implementation, as they fail to raise. + raise exception.InvalidParameterValue(err=err) + + if len(str(group_name)) > 255: + err = _("Value (%s) for parameter GroupName is invalid." + " Length exceeds maximum of 255.") % group_name + raise exception.InvalidParameterValue(err=err) + LOG.audit(_("Create Security Group %s"), group_name, context=context) self.compute_api.ensure_default_security_group(context) if db.security_group_exists(context, context.project_id, group_name): @@ -948,19 +995,113 @@ class CloudController(object): 'status': volume['attach_status'], 'volumeId': ec2utils.id_to_ec2_vol_id(volume_id)} - def _convert_to_set(self, lst, label): + @staticmethod + def _convert_to_set(lst, label): if lst is None or lst == []: return None if not isinstance(lst, list): lst = [lst] return [{label: x} for x in lst] + def _format_kernel_id(self, instance_ref, result, key): + kernel_id = instance_ref['kernel_id'] + if kernel_id is None: + return + result[key] = self.image_ec2_id(instance_ref['kernel_id'], 'aki') + + def _format_ramdisk_id(self, instance_ref, result, key): + ramdisk_id = instance_ref['ramdisk_id'] + if ramdisk_id is None: + return + result[key] = self.image_ec2_id(instance_ref['ramdisk_id'], 'ari') + + @staticmethod + def _format_user_data(instance_ref): + return base64.b64decode(instance_ref['user_data']) + + def describe_instance_attribute(self, context, instance_id, attribute, + **kwargs): + def _unsupported_attribute(instance, result): + raise exception.ApiError(_('attribute not supported: %s') % + attribute) + + def _format_attr_block_device_mapping(instance, result): + tmp = {} + self._format_instance_root_device_name(instance, tmp) + self._format_instance_bdm(context, instance_id, + tmp['rootDeviceName'], result) + + def _format_attr_disable_api_termination(instance, result): + _unsupported_attribute(instance, result) + + def _format_attr_group_set(instance, result): + CloudController._format_group_set(instance, result) + + def _format_attr_instance_initiated_shutdown_behavior(instance, + result): + state_description = instance['state_description'] + state_to_value = {'stopping': 'stop', + 'stopped': 'stop', + 'terminating': 'terminate'} + value = state_to_value.get(state_description) + if value: + result['instanceInitiatedShutdownBehavior'] = value + + def _format_attr_instance_type(instance, result): + self._format_instance_type(instance, result) + + def _format_attr_kernel(instance, result): + self._format_kernel_id(instance, result, 'kernel') + + def _format_attr_ramdisk(instance, result): + self._format_ramdisk_id(instance, result, 'ramdisk') + + def _format_attr_root_device_name(instance, result): + self._format_instance_root_device_name(instance, result) + + def _format_attr_source_dest_check(instance, result): + _unsupported_attribute(instance, result) + + def _format_attr_user_data(instance, result): + result['userData'] = self._format_user_data(instance) + + attribute_formatter = { + 'blockDeviceMapping': _format_attr_block_device_mapping, + 'disableApiTermination': _format_attr_disable_api_termination, + 'groupSet': _format_attr_group_set, + 'instanceInitiatedShutdownBehavior': + _format_attr_instance_initiated_shutdown_behavior, + 'instanceType': _format_attr_instance_type, + 'kernel': _format_attr_kernel, + 'ramdisk': _format_attr_ramdisk, + 'rootDeviceName': _format_attr_root_device_name, + 'sourceDestCheck': _format_attr_source_dest_check, + 'userData': _format_attr_user_data, + } + + fn = attribute_formatter.get(attribute) + if fn is None: + raise exception.ApiError( + _('attribute not supported: %s') % attribute) + + ec2_instance_id = instance_id + instance_id = ec2utils.ec2_id_to_id(ec2_instance_id) + instance = self.compute_api.get(context, instance_id) + result = {'instance_id': ec2_instance_id} + fn(instance, result) + return result + def describe_instances(self, context, **kwargs): - return self._format_describe_instances(context, **kwargs) + # Optional DescribeInstances argument + instance_id = kwargs.get('instance_id', None) + return self._format_describe_instances(context, + instance_id=instance_id) def describe_instances_v6(self, context, **kwargs): - kwargs['use_v6'] = True - return self._format_describe_instances(context, **kwargs) + # Optional DescribeInstancesV6 argument + instance_id = kwargs.get('instance_id', None) + return self._format_describe_instances(context, + instance_id=instance_id, use_v6=True) def _format_describe_instances(self, context, **kwargs): return {'reservationSet': self._format_instances(context, **kwargs)} @@ -1001,7 +1142,29 @@ class CloudController(object): result['blockDeviceMapping'] = mapping result['rootDeviceType'] = root_device_type - def _format_instances(self, context, instance_id=None, **kwargs): + @staticmethod + def _format_instance_root_device_name(instance, result): + result['rootDeviceName'] = (instance.get('root_device_name') or + _DEFAULT_ROOT_DEVICE_NAME) + + @staticmethod + def _format_instance_type(instance, result): + if instance['instance_type']: + result['instanceType'] = instance['instance_type'].get('name') + else: + result['instanceType'] = None + + @staticmethod + def _format_group_set(instance, result): + security_group_names = [] + if instance.get('security_groups'): + for security_group in instance['security_groups']: + security_group_names.append(security_group['name']) + result['groupSet'] = CloudController._convert_to_set( + security_group_names, 'groupId') + + def _format_instances(self, context, instance_id=None, use_v6=False, + **search_opts): # TODO(termie): this method is poorly named as its name does not imply # that it will be making a variety of database calls # rather than simply formatting a bunch of instances that @@ -1012,11 +1175,17 @@ class CloudController(object): instances = [] for ec2_id in instance_id: internal_id = ec2utils.ec2_id_to_id(ec2_id) - instance = self.compute_api.get(context, - instance_id=internal_id) + try: + instance = self.compute_api.get(context, internal_id) + except exception.NotFound: + continue instances.append(instance) else: - instances = self.compute_api.get_all(context, **kwargs) + try: + instances = self.compute_api.get_all(context, + search_opts=search_opts) + except exception.NotFound: + instances = [] for instance in instances: if not context.is_admin: if instance['image_ref'] == str(FLAGS.vpn_image_id): @@ -1026,6 +1195,8 @@ class CloudController(object): ec2_id = ec2utils.id_to_ec2_id(instance_id) i['instanceId'] = ec2_id i['imageId'] = self.image_ec2_id(instance['image_ref']) + self._format_kernel_id(instance, i, 'kernelId') + self._format_ramdisk_id(instance, i, 'ramdiskId') i['instanceState'] = { 'code': instance['state'], 'name': instance['state_description']} @@ -1036,7 +1207,7 @@ class CloudController(object): fixed_addr = fixed['address'] if fixed['floating_ips']: floating_addr = fixed['floating_ips'][0]['address'] - if fixed['network'] and 'use_v6' in kwargs: + if fixed['network'] and use_v6: i['dnsNameV6'] = ipv6.to_global( fixed['network']['cidr_v6'], fixed['virtual_interface']['address'], @@ -1054,16 +1225,12 @@ class CloudController(object): instance['project_id'], instance['host']) i['productCodesSet'] = self._convert_to_set([], 'product_codes') - if instance['instance_type']: - i['instanceType'] = instance['instance_type'].get('name') - else: - i['instanceType'] = None + self._format_instance_type(instance, i) i['launchTime'] = instance['created_at'] i['amiLaunchIndex'] = instance['launch_index'] i['displayName'] = instance['display_name'] i['displayDescription'] = instance['display_description'] - i['rootDeviceName'] = (instance.get('root_device_name') or - _DEFAULT_ROOT_DEVICE_NAME) + self._format_instance_root_device_name(instance, i) self._format_instance_bdm(context, instance_id, i['rootDeviceName'], i) host = instance['host'] @@ -1073,12 +1240,7 @@ class CloudController(object): r = {} r['reservationId'] = instance['reservation_id'] r['ownerId'] = instance['project_id'] - security_group_names = [] - if instance.get('security_groups'): - for security_group in instance['security_groups']: - security_group_names.append(security_group['name']) - r['groupSet'] = self._convert_to_set(security_group_names, - 'groupId') + self._format_group_set(instance, r) r['instancesSet'] = [] reservations[instance['reservation_id']] = r reservations[instance['reservation_id']]['instancesSet'].append(i) @@ -1182,7 +1344,7 @@ class CloudController(object): 'AvailabilityZone'), block_device_mapping=kwargs.get('block_device_mapping', {})) return self._format_run_instances(context, - instances[0]['reservation_id']) + reservation_id=instances[0]['reservation_id']) def _do_instance(self, action, context, ec2_id): instance_id = ec2utils.ec2_id_to_id(ec2_id) @@ -1314,7 +1476,7 @@ class CloudController(object): i['architecture'] = image['properties'].get('architecture') properties = image['properties'] - root_device_name = ec2utils.properties_root_device_name(properties) + root_device_name = block_device.properties_root_device_name(properties) root_device_type = 'instance-store' for bdm in properties.get('block_device_mapping', []): if (bdm.get('device_name') == root_device_name and @@ -1387,7 +1549,7 @@ class CloudController(object): def _root_device_name_attribute(image, result): result['rootDeviceName'] = \ - ec2utils.properties_root_device_name(image['properties']) + block_device.properties_root_device_name(image['properties']) if result['rootDeviceName'] is None: result['rootDeviceName'] = _DEFAULT_ROOT_DEVICE_NAME @@ -1520,8 +1682,7 @@ class CloudController(object): if virtual_name in ('ami', 'root'): continue - assert (virtual_name == 'swap' or - virtual_name.startswith('ephemeral')) + assert block_device.is_swap_or_ephemeral(virtual_name) device_name = m['device'] if device_name in [b['device_name'] for b in mapping if not b.get('no_device', False)]: diff --git a/nova/api/ec2/ec2utils.py b/nova/api/ec2/ec2utils.py index bae1e0ee5..bcdf2ba78 100644 --- a/nova/api/ec2/ec2utils.py +++ b/nova/api/ec2/ec2utils.py @@ -135,32 +135,3 @@ def dict_from_dotted_str(items): args[key] = value return args - - -def properties_root_device_name(properties): - """get root device name from image meta data. - If it isn't specified, return None. - """ - root_device_name = None - - # NOTE(yamahata): see image_service.s3.s3create() - for bdm in properties.get('mappings', []): - if bdm['virtual'] == 'root': - root_device_name = bdm['device'] - - # NOTE(yamahata): register_image's command line can override - # <machine>.manifest.xml - if 'root_device_name' in properties: - root_device_name = properties['root_device_name'] - - return root_device_name - - -def mappings_prepend_dev(mappings): - """Prepend '/dev/' to 'device' entry of swap/ephemeral virtual type""" - for m in mappings: - virtual = m['virtual'] - if ((virtual == 'swap' or virtual.startswith('ephemeral')) and - (not m['device'].startswith('/'))): - m['device'] = '/dev/' + m['device'] - return mappings diff --git a/nova/api/openstack/common.py b/nova/api/openstack/common.py index 5226cdf9a..dfdd62201 100644 --- a/nova/api/openstack/common.py +++ b/nova/api/openstack/common.py @@ -25,7 +25,9 @@ import webob from nova import exception from nova import flags from nova import log as logging +from nova import quota from nova.api.openstack import wsgi +from nova.compute import power_state as compute_power_state LOG = logging.getLogger('nova.api.openstack.common') @@ -36,6 +38,38 @@ XML_NS_V10 = 'http://docs.rackspacecloud.com/servers/api/v1.0' XML_NS_V11 = 'http://docs.openstack.org/compute/api/v1.1' +_STATUS_MAP = { + None: 'BUILD', + compute_power_state.NOSTATE: 'BUILD', + compute_power_state.RUNNING: 'ACTIVE', + compute_power_state.BLOCKED: 'ACTIVE', + compute_power_state.SUSPENDED: 'SUSPENDED', + compute_power_state.PAUSED: 'PAUSED', + compute_power_state.SHUTDOWN: 'SHUTDOWN', + compute_power_state.SHUTOFF: 'SHUTOFF', + compute_power_state.CRASHED: 'ERROR', + compute_power_state.FAILED: 'ERROR', + compute_power_state.BUILDING: 'BUILD', +} + + +def status_from_power_state(power_state): + """Map the power state to the server status string""" + return _STATUS_MAP[power_state] + + +def power_states_from_status(status): + """Map the server status string to a list of power states""" + power_states = [] + for power_state, status_map in _STATUS_MAP.iteritems(): + # Skip the 'None' state + if power_state is None: + continue + if status.lower() == status_map.lower(): + power_states.append(power_state) + return power_states + + def get_pagination_params(request): """Return marker, limit tuple from request. @@ -156,7 +190,7 @@ def remove_version_from_href(href): """ parsed_url = urlparse.urlsplit(href) new_path = re.sub(r'^/v[0-9]+\.[0-9]+(/|$)', r'\1', parsed_url.path, - count=1) + count=1) if new_path == parsed_url.path: msg = _('href %s does not contain version') % href @@ -193,6 +227,16 @@ def get_version_from_href(href): return version +def check_img_metadata_quota_limit(context, metadata): + if metadata is None: + return + num_metadata = len(metadata) + quota_metadata = quota.allowed_metadata_items(context, num_metadata) + if quota_metadata < num_metadata: + expl = _("Image metadata limit exceeded") + raise webob.exc.HTTPBadRequest(explanation=expl) + + class MetadataXMLDeserializer(wsgi.XMLDeserializer): def extract_metadata(self, metadata_node): diff --git a/nova/api/openstack/contrib/admin_only.py b/nova/api/openstack/contrib/admin_only.py new file mode 100644 index 000000000..e821c9e1f --- /dev/null +++ b/nova/api/openstack/contrib/admin_only.py @@ -0,0 +1,30 @@ +# Copyright (c) 2011 Openstack, LLC. +# 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. + +"""Decorator for limiting extensions that should be admin-only.""" + +from functools import wraps +from nova import flags +FLAGS = flags.FLAGS + + +def admin_only(fnc): + @wraps(fnc) + def _wrapped(self, *args, **kwargs): + if FLAGS.allow_admin_api: + return fnc(self, *args, **kwargs) + return [] + _wrapped.func_name = fnc.func_name + return _wrapped diff --git a/nova/api/openstack/contrib/floating_ips.py b/nova/api/openstack/contrib/floating_ips.py index 3d8049324..52c9c6cf9 100644 --- a/nova/api/openstack/contrib/floating_ips.py +++ b/nova/api/openstack/contrib/floating_ips.py @@ -18,12 +18,16 @@ from webob import exc from nova import exception +from nova import log as logging from nova import network from nova import rpc from nova.api.openstack import faults from nova.api.openstack import extensions +LOG = logging.getLogger('nova.api.openstack.contrib.floating_ips') + + def _translate_floating_ip_view(floating_ip): result = {'id': floating_ip['id'], 'ip': floating_ip['address']} @@ -97,8 +101,14 @@ class FloatingIPController(object): def delete(self, req, id): context = req.environ['nova.context'] - ip = self.network_api.get_floating_ip(context, id) + + if 'fixed_ip' in ip: + try: + self.disassociate(req, id, '') + except Exception as e: + LOG.exception(_("Error disassociating fixed_ip %s"), e) + self.network_api.release_floating_ip(context, address=ip) return {'released': { diff --git a/nova/api/openstack/contrib/hosts.py b/nova/api/openstack/contrib/hosts.py index 55e57e1a4..ecaa365b7 100644 --- a/nova/api/openstack/contrib/hosts.py +++ b/nova/api/openstack/contrib/hosts.py @@ -24,6 +24,7 @@ from nova import log as logging from nova.api.openstack import common from nova.api.openstack import extensions from nova.api.openstack import faults +from nova.api.openstack.contrib import admin_only from nova.scheduler import api as scheduler_api @@ -70,7 +71,7 @@ class HostController(object): key = raw_key.lower().strip() val = raw_val.lower().strip() # NOTE: (dabo) Right now only 'status' can be set, but other - # actions may follow. + # settings may follow. if key == "status": if val[:6] in ("enable", "disabl"): return self._set_enabled_status(req, id, @@ -89,8 +90,30 @@ class HostController(object): LOG.audit(_("Setting host %(host)s to %(state)s.") % locals()) result = self.compute_api.set_host_enabled(context, host=host, enabled=enabled) + if result not in ("enabled", "disabled"): + # An error message was returned + raise webob.exc.HTTPBadRequest(explanation=result) return {"host": host, "status": result} + def _host_power_action(self, req, host, action): + """Reboots, shuts down or powers up the host.""" + context = req.environ['nova.context'] + try: + result = self.compute_api.host_power_action(context, host=host, + action=action) + except NotImplementedError as e: + raise webob.exc.HTTPBadRequest(explanation=e.msg) + return {"host": host, "power_action": result} + + def startup(self, req, id): + return self._host_power_action(req, host=id, action="startup") + + def shutdown(self, req, id): + return self._host_power_action(req, host=id, action="shutdown") + + def reboot(self, req, id): + return self._host_power_action(req, host=id, action="reboot") + class Hosts(extensions.ExtensionDescriptor): def get_name(self): @@ -108,7 +131,10 @@ class Hosts(extensions.ExtensionDescriptor): def get_updated(self): return "2011-06-29T00:00:00+00:00" + @admin_only.admin_only def get_resources(self): - resources = [extensions.ResourceExtension('os-hosts', HostController(), - collection_actions={'update': 'PUT'}, member_actions={})] + resources = [extensions.ResourceExtension('os-hosts', + HostController(), collection_actions={'update': 'PUT'}, + member_actions={"startup": "GET", "shutdown": "GET", + "reboot": "GET"})] return resources diff --git a/nova/api/openstack/extensions.py b/nova/api/openstack/extensions.py index dbf922dbb..8daf12343 100644 --- a/nova/api/openstack/extensions.py +++ b/nova/api/openstack/extensions.py @@ -23,7 +23,7 @@ import sys import routes import webob.dec import webob.exc -from xml.etree import ElementTree +from lxml import etree from nova import exception from nova import flags @@ -32,6 +32,7 @@ from nova import wsgi as base_wsgi from nova.api.openstack import common from nova.api.openstack import faults from nova.api.openstack import wsgi +from nova.api.openstack import xmlutil LOG = logging.getLogger('extensions') @@ -478,36 +479,38 @@ class ResourceExtension(object): class ExtensionsXMLSerializer(wsgi.XMLDictSerializer): + NSMAP = {None: xmlutil.XMLNS_V11, 'atom': xmlutil.XMLNS_ATOM} + def show(self, ext_dict): - ext = self._create_ext_elem(ext_dict['extension']) + ext = etree.Element('extension', nsmap=self.NSMAP) + self._populate_ext(ext, ext_dict['extension']) return self._to_xml(ext) def index(self, exts_dict): - exts = ElementTree.Element('extensions') + exts = etree.Element('extensions', nsmap=self.NSMAP) for ext_dict in exts_dict['extensions']: - exts.append(self._create_ext_elem(ext_dict)) + ext = etree.SubElement(exts, 'extension') + self._populate_ext(ext, ext_dict) return self._to_xml(exts) - def _create_ext_elem(self, ext_dict): - """Create an extension xml element from a dict.""" - ext_elem = ElementTree.Element('extension') + def _populate_ext(self, ext_elem, ext_dict): + """Populate an extension xml element from a dict.""" + ext_elem.set('name', ext_dict['name']) ext_elem.set('namespace', ext_dict['namespace']) ext_elem.set('alias', ext_dict['alias']) ext_elem.set('updated', ext_dict['updated']) - desc = ElementTree.Element('description') + desc = etree.Element('description') desc.text = ext_dict['description'] ext_elem.append(desc) for link in ext_dict.get('links', []): - elem = ElementTree.Element('atom:link') + elem = etree.SubElement(ext_elem, '{%s}link' % xmlutil.XMLNS_ATOM) elem.set('rel', link['rel']) elem.set('href', link['href']) elem.set('type', link['type']) - ext_elem.append(elem) return ext_elem def _to_xml(self, root): - """Convert the xml tree object to an xml string.""" - root.set('xmlns', wsgi.XMLNS_V11) - root.set('xmlns:atom', wsgi.XMLNS_ATOM) - return ElementTree.tostring(root, encoding='UTF-8') + """Convert the xml object to an xml string.""" + + return etree.tostring(root, encoding='UTF-8') diff --git a/nova/api/openstack/image_metadata.py b/nova/api/openstack/image_metadata.py index aaf64a123..4d615ea96 100644 --- a/nova/api/openstack/image_metadata.py +++ b/nova/api/openstack/image_metadata.py @@ -19,7 +19,6 @@ from webob import exc from nova import flags from nova import image -from nova import quota from nova import utils from nova.api.openstack import common from nova.api.openstack import wsgi @@ -40,15 +39,6 @@ class Controller(object): metadata = image.get('properties', {}) return metadata - def _check_quota_limit(self, context, metadata): - if metadata is None: - return - num_metadata = len(metadata) - quota_metadata = quota.allowed_metadata_items(context, num_metadata) - if quota_metadata < num_metadata: - expl = _("Image metadata limit exceeded") - raise exc.HTTPBadRequest(explanation=expl) - def index(self, req, image_id): """Returns the list of metadata for a given instance""" context = req.environ['nova.context'] @@ -70,7 +60,7 @@ class Controller(object): if 'metadata' in body: for key, value in body['metadata'].iteritems(): metadata[key] = value - self._check_quota_limit(context, metadata) + common.check_img_metadata_quota_limit(context, metadata) img['properties'] = metadata self.image_service.update(context, image_id, img, None) return dict(metadata=metadata) @@ -93,7 +83,7 @@ class Controller(object): img = self.image_service.show(context, image_id) metadata = self._get_metadata(context, image_id, img) metadata[id] = meta[id] - self._check_quota_limit(context, metadata) + common.check_img_metadata_quota_limit(context, metadata) img['properties'] = metadata self.image_service.update(context, image_id, img, None) return dict(meta=meta) @@ -102,7 +92,7 @@ class Controller(object): context = req.environ['nova.context'] img = self.image_service.show(context, image_id) metadata = body.get('metadata', {}) - self._check_quota_limit(context, metadata) + common.check_img_metadata_quota_limit(context, metadata) img['properties'] = metadata self.image_service.update(context, image_id, img, None) return dict(metadata=metadata) diff --git a/nova/api/openstack/schemas/atom-link.rng b/nova/api/openstack/schemas/atom-link.rng new file mode 100644 index 000000000..edba5eee6 --- /dev/null +++ b/nova/api/openstack/schemas/atom-link.rng @@ -0,0 +1,141 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + -*- rnc -*- + RELAX NG Compact Syntax Grammar for the + Atom Format Specification Version 11 +--> +<grammar xmlns:xhtml="http://www.w3.org/1999/xhtml" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:s="http://www.ascc.net/xml/schematron" xmlns="http://relaxng.org/ns/structure/1.0" datatypeLibrary="http://www.w3.org/2001/XMLSchema-datatypes"> + <start> + <choice> + <ref name="atomLink"/> + </choice> + </start> + <!-- Common attributes --> + <define name="atomCommonAttributes"> + <optional> + <attribute name="xml:base"> + <ref name="atomUri"/> + </attribute> + </optional> + <optional> + <attribute name="xml:lang"> + <ref name="atomLanguageTag"/> + </attribute> + </optional> + <zeroOrMore> + <ref name="undefinedAttribute"/> + </zeroOrMore> + </define> + <!-- atom:link --> + <define name="atomLink"> + <element name="atom:link"> + <ref name="atomCommonAttributes"/> + <attribute name="href"> + <ref name="atomUri"/> + </attribute> + <optional> + <attribute name="rel"> + <choice> + <ref name="atomNCName"/> + <ref name="atomUri"/> + </choice> + </attribute> + </optional> + <optional> + <attribute name="type"> + <ref name="atomMediaType"/> + </attribute> + </optional> + <optional> + <attribute name="hreflang"> + <ref name="atomLanguageTag"/> + </attribute> + </optional> + <optional> + <attribute name="title"/> + </optional> + <optional> + <attribute name="length"/> + </optional> + <ref name="undefinedContent"/> + </element> + </define> + <!-- Low-level simple types --> + <define name="atomNCName"> + <data type="string"> + <param name="minLength">1</param> + <param name="pattern">[^:]*</param> + </data> + </define> + <!-- Whatever a media type is, it contains at least one slash --> + <define name="atomMediaType"> + <data type="string"> + <param name="pattern">.+/.+</param> + </data> + </define> + <!-- As defined in RFC 3066 --> + <define name="atomLanguageTag"> + <data type="string"> + <param name="pattern">[A-Za-z]{1,8}(-[A-Za-z0-9]{1,8})*</param> + </data> + </define> + <!-- + Unconstrained; it's not entirely clear how IRI fit into + xsd:anyURI so let's not try to constrain it here + --> + <define name="atomUri"> + <text/> + </define> + <!-- Other Extensibility --> + <define name="undefinedAttribute"> + <attribute> + <anyName> + <except> + <name>xml:base</name> + <name>xml:lang</name> + <nsName ns=""/> + </except> + </anyName> + </attribute> + </define> + <define name="undefinedContent"> + <zeroOrMore> + <choice> + <text/> + <ref name="anyForeignElement"/> + </choice> + </zeroOrMore> + </define> + <define name="anyElement"> + <element> + <anyName/> + <zeroOrMore> + <choice> + <attribute> + <anyName/> + </attribute> + <text/> + <ref name="anyElement"/> + </choice> + </zeroOrMore> + </element> + </define> + <define name="anyForeignElement"> + <element> + <anyName> + <except> + <nsName ns="http://www.w3.org/2005/Atom"/> + </except> + </anyName> + <zeroOrMore> + <choice> + <attribute> + <anyName/> + </attribute> + <text/> + <ref name="anyElement"/> + </choice> + </zeroOrMore> + </element> + </define> +</grammar> diff --git a/nova/api/openstack/schemas/atom.rng b/nova/api/openstack/schemas/atom.rng new file mode 100644 index 000000000..c2df4e410 --- /dev/null +++ b/nova/api/openstack/schemas/atom.rng @@ -0,0 +1,597 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + -*- rnc -*- + RELAX NG Compact Syntax Grammar for the + Atom Format Specification Version 11 +--> +<grammar xmlns:xhtml="http://www.w3.org/1999/xhtml" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:s="http://www.ascc.net/xml/schematron" xmlns="http://relaxng.org/ns/structure/1.0" datatypeLibrary="http://www.w3.org/2001/XMLSchema-datatypes"> + <start> + <choice> + <ref name="atomFeed"/> + <ref name="atomEntry"/> + </choice> + </start> + <!-- Common attributes --> + <define name="atomCommonAttributes"> + <optional> + <attribute name="xml:base"> + <ref name="atomUri"/> + </attribute> + </optional> + <optional> + <attribute name="xml:lang"> + <ref name="atomLanguageTag"/> + </attribute> + </optional> + <zeroOrMore> + <ref name="undefinedAttribute"/> + </zeroOrMore> + </define> + <!-- Text Constructs --> + <define name="atomPlainTextConstruct"> + <ref name="atomCommonAttributes"/> + <optional> + <attribute name="type"> + <choice> + <value>text</value> + <value>html</value> + </choice> + </attribute> + </optional> + <text/> + </define> + <define name="atomXHTMLTextConstruct"> + <ref name="atomCommonAttributes"/> + <attribute name="type"> + <value>xhtml</value> + </attribute> + <ref name="xhtmlDiv"/> + </define> + <define name="atomTextConstruct"> + <choice> + <ref name="atomPlainTextConstruct"/> + <ref name="atomXHTMLTextConstruct"/> + </choice> + </define> + <!-- Person Construct --> + <define name="atomPersonConstruct"> + <ref name="atomCommonAttributes"/> + <interleave> + <element name="atom:name"> + <text/> + </element> + <optional> + <element name="atom:uri"> + <ref name="atomUri"/> + </element> + </optional> + <optional> + <element name="atom:email"> + <ref name="atomEmailAddress"/> + </element> + </optional> + <zeroOrMore> + <ref name="extensionElement"/> + </zeroOrMore> + </interleave> + </define> + <!-- Date Construct --> + <define name="atomDateConstruct"> + <ref name="atomCommonAttributes"/> + <data type="dateTime"/> + </define> + <!-- atom:feed --> + <define name="atomFeed"> + <element name="atom:feed"> + <s:rule context="atom:feed"> + <s:assert test="atom:author or not(atom:entry[not(atom:author)])">An atom:feed must have an atom:author unless all of its atom:entry children have an atom:author.</s:assert> + </s:rule> + <ref name="atomCommonAttributes"/> + <interleave> + <zeroOrMore> + <ref name="atomAuthor"/> + </zeroOrMore> + <zeroOrMore> + <ref name="atomCategory"/> + </zeroOrMore> + <zeroOrMore> + <ref name="atomContributor"/> + </zeroOrMore> + <optional> + <ref name="atomGenerator"/> + </optional> + <optional> + <ref name="atomIcon"/> + </optional> + <ref name="atomId"/> + <zeroOrMore> + <ref name="atomLink"/> + </zeroOrMore> + <optional> + <ref name="atomLogo"/> + </optional> + <optional> + <ref name="atomRights"/> + </optional> + <optional> + <ref name="atomSubtitle"/> + </optional> + <ref name="atomTitle"/> + <ref name="atomUpdated"/> + <zeroOrMore> + <ref name="extensionElement"/> + </zeroOrMore> + </interleave> + <zeroOrMore> + <ref name="atomEntry"/> + </zeroOrMore> + </element> + </define> + <!-- atom:entry --> + <define name="atomEntry"> + <element name="atom:entry"> + <s:rule context="atom:entry"> + <s:assert test="atom:link[@rel='alternate'] or atom:link[not(@rel)] or atom:content">An atom:entry must have at least one atom:link element with a rel attribute of 'alternate' or an atom:content.</s:assert> + </s:rule> + <s:rule context="atom:entry"> + <s:assert test="atom:author or ../atom:author or atom:source/atom:author">An atom:entry must have an atom:author if its feed does not.</s:assert> + </s:rule> + <ref name="atomCommonAttributes"/> + <interleave> + <zeroOrMore> + <ref name="atomAuthor"/> + </zeroOrMore> + <zeroOrMore> + <ref name="atomCategory"/> + </zeroOrMore> + <optional> + <ref name="atomContent"/> + </optional> + <zeroOrMore> + <ref name="atomContributor"/> + </zeroOrMore> + <ref name="atomId"/> + <zeroOrMore> + <ref name="atomLink"/> + </zeroOrMore> + <optional> + <ref name="atomPublished"/> + </optional> + <optional> + <ref name="atomRights"/> + </optional> + <optional> + <ref name="atomSource"/> + </optional> + <optional> + <ref name="atomSummary"/> + </optional> + <ref name="atomTitle"/> + <ref name="atomUpdated"/> + <zeroOrMore> + <ref name="extensionElement"/> + </zeroOrMore> + </interleave> + </element> + </define> + <!-- atom:content --> + <define name="atomInlineTextContent"> + <element name="atom:content"> + <ref name="atomCommonAttributes"/> + <optional> + <attribute name="type"> + <choice> + <value>text</value> + <value>html</value> + </choice> + </attribute> + </optional> + <zeroOrMore> + <text/> + </zeroOrMore> + </element> + </define> + <define name="atomInlineXHTMLContent"> + <element name="atom:content"> + <ref name="atomCommonAttributes"/> + <attribute name="type"> + <value>xhtml</value> + </attribute> + <ref name="xhtmlDiv"/> + </element> + </define> + <define name="atomInlineOtherContent"> + <element name="atom:content"> + <ref name="atomCommonAttributes"/> + <optional> + <attribute name="type"> + <ref name="atomMediaType"/> + </attribute> + </optional> + <zeroOrMore> + <choice> + <text/> + <ref name="anyElement"/> + </choice> + </zeroOrMore> + </element> + </define> + <define name="atomOutOfLineContent"> + <element name="atom:content"> + <ref name="atomCommonAttributes"/> + <optional> + <attribute name="type"> + <ref name="atomMediaType"/> + </attribute> + </optional> + <attribute name="src"> + <ref name="atomUri"/> + </attribute> + <empty/> + </element> + </define> + <define name="atomContent"> + <choice> + <ref name="atomInlineTextContent"/> + <ref name="atomInlineXHTMLContent"/> + <ref name="atomInlineOtherContent"/> + <ref name="atomOutOfLineContent"/> + </choice> + </define> + <!-- atom:author --> + <define name="atomAuthor"> + <element name="atom:author"> + <ref name="atomPersonConstruct"/> + </element> + </define> + <!-- atom:category --> + <define name="atomCategory"> + <element name="atom:category"> + <ref name="atomCommonAttributes"/> + <attribute name="term"/> + <optional> + <attribute name="scheme"> + <ref name="atomUri"/> + </attribute> + </optional> + <optional> + <attribute name="label"/> + </optional> + <ref name="undefinedContent"/> + </element> + </define> + <!-- atom:contributor --> + <define name="atomContributor"> + <element name="atom:contributor"> + <ref name="atomPersonConstruct"/> + </element> + </define> + <!-- atom:generator --> + <define name="atomGenerator"> + <element name="atom:generator"> + <ref name="atomCommonAttributes"/> + <optional> + <attribute name="uri"> + <ref name="atomUri"/> + </attribute> + </optional> + <optional> + <attribute name="version"/> + </optional> + <text/> + </element> + </define> + <!-- atom:icon --> + <define name="atomIcon"> + <element name="atom:icon"> + <ref name="atomCommonAttributes"/> + <ref name="atomUri"/> + </element> + </define> + <!-- atom:id --> + <define name="atomId"> + <element name="atom:id"> + <ref name="atomCommonAttributes"/> + <ref name="atomUri"/> + </element> + </define> + <!-- atom:logo --> + <define name="atomLogo"> + <element name="atom:logo"> + <ref name="atomCommonAttributes"/> + <ref name="atomUri"/> + </element> + </define> + <!-- atom:link --> + <define name="atomLink"> + <element name="atom:link"> + <ref name="atomCommonAttributes"/> + <attribute name="href"> + <ref name="atomUri"/> + </attribute> + <optional> + <attribute name="rel"> + <choice> + <ref name="atomNCName"/> + <ref name="atomUri"/> + </choice> + </attribute> + </optional> + <optional> + <attribute name="type"> + <ref name="atomMediaType"/> + </attribute> + </optional> + <optional> + <attribute name="hreflang"> + <ref name="atomLanguageTag"/> + </attribute> + </optional> + <optional> + <attribute name="title"/> + </optional> + <optional> + <attribute name="length"/> + </optional> + <ref name="undefinedContent"/> + </element> + </define> + <!-- atom:published --> + <define name="atomPublished"> + <element name="atom:published"> + <ref name="atomDateConstruct"/> + </element> + </define> + <!-- atom:rights --> + <define name="atomRights"> + <element name="atom:rights"> + <ref name="atomTextConstruct"/> + </element> + </define> + <!-- atom:source --> + <define name="atomSource"> + <element name="atom:source"> + <ref name="atomCommonAttributes"/> + <interleave> + <zeroOrMore> + <ref name="atomAuthor"/> + </zeroOrMore> + <zeroOrMore> + <ref name="atomCategory"/> + </zeroOrMore> + <zeroOrMore> + <ref name="atomContributor"/> + </zeroOrMore> + <optional> + <ref name="atomGenerator"/> + </optional> + <optional> + <ref name="atomIcon"/> + </optional> + <optional> + <ref name="atomId"/> + </optional> + <zeroOrMore> + <ref name="atomLink"/> + </zeroOrMore> + <optional> + <ref name="atomLogo"/> + </optional> + <optional> + <ref name="atomRights"/> + </optional> + <optional> + <ref name="atomSubtitle"/> + </optional> + <optional> + <ref name="atomTitle"/> + </optional> + <optional> + <ref name="atomUpdated"/> + </optional> + <zeroOrMore> + <ref name="extensionElement"/> + </zeroOrMore> + </interleave> + </element> + </define> + <!-- atom:subtitle --> + <define name="atomSubtitle"> + <element name="atom:subtitle"> + <ref name="atomTextConstruct"/> + </element> + </define> + <!-- atom:summary --> + <define name="atomSummary"> + <element name="atom:summary"> + <ref name="atomTextConstruct"/> + </element> + </define> + <!-- atom:title --> + <define name="atomTitle"> + <element name="atom:title"> + <ref name="atomTextConstruct"/> + </element> + </define> + <!-- atom:updated --> + <define name="atomUpdated"> + <element name="atom:updated"> + <ref name="atomDateConstruct"/> + </element> + </define> + <!-- Low-level simple types --> + <define name="atomNCName"> + <data type="string"> + <param name="minLength">1</param> + <param name="pattern">[^:]*</param> + </data> + </define> + <!-- Whatever a media type is, it contains at least one slash --> + <define name="atomMediaType"> + <data type="string"> + <param name="pattern">.+/.+</param> + </data> + </define> + <!-- As defined in RFC 3066 --> + <define name="atomLanguageTag"> + <data type="string"> + <param name="pattern">[A-Za-z]{1,8}(-[A-Za-z0-9]{1,8})*</param> + </data> + </define> + <!-- + Unconstrained; it's not entirely clear how IRI fit into + xsd:anyURI so let's not try to constrain it here + --> + <define name="atomUri"> + <text/> + </define> + <!-- Whatever an email address is, it contains at least one @ --> + <define name="atomEmailAddress"> + <data type="string"> + <param name="pattern">.+@.+</param> + </data> + </define> + <!-- Simple Extension --> + <define name="simpleExtensionElement"> + <element> + <anyName> + <except> + <nsName ns="http://www.w3.org/2005/Atom"/> + </except> + </anyName> + <text/> + </element> + </define> + <!-- Structured Extension --> + <define name="structuredExtensionElement"> + <element> + <anyName> + <except> + <nsName ns="http://www.w3.org/2005/Atom"/> + </except> + </anyName> + <choice> + <group> + <oneOrMore> + <attribute> + <anyName/> + </attribute> + </oneOrMore> + <zeroOrMore> + <choice> + <text/> + <ref name="anyElement"/> + </choice> + </zeroOrMore> + </group> + <group> + <zeroOrMore> + <attribute> + <anyName/> + </attribute> + </zeroOrMore> + <group> + <optional> + <text/> + </optional> + <oneOrMore> + <ref name="anyElement"/> + </oneOrMore> + <zeroOrMore> + <choice> + <text/> + <ref name="anyElement"/> + </choice> + </zeroOrMore> + </group> + </group> + </choice> + </element> + </define> + <!-- Other Extensibility --> + <define name="extensionElement"> + <choice> + <ref name="simpleExtensionElement"/> + <ref name="structuredExtensionElement"/> + </choice> + </define> + <define name="undefinedAttribute"> + <attribute> + <anyName> + <except> + <name>xml:base</name> + <name>xml:lang</name> + <nsName ns=""/> + </except> + </anyName> + </attribute> + </define> + <define name="undefinedContent"> + <zeroOrMore> + <choice> + <text/> + <ref name="anyForeignElement"/> + </choice> + </zeroOrMore> + </define> + <define name="anyElement"> + <element> + <anyName/> + <zeroOrMore> + <choice> + <attribute> + <anyName/> + </attribute> + <text/> + <ref name="anyElement"/> + </choice> + </zeroOrMore> + </element> + </define> + <define name="anyForeignElement"> + <element> + <anyName> + <except> + <nsName ns="http://www.w3.org/2005/Atom"/> + </except> + </anyName> + <zeroOrMore> + <choice> + <attribute> + <anyName/> + </attribute> + <text/> + <ref name="anyElement"/> + </choice> + </zeroOrMore> + </element> + </define> + <!-- XHTML --> + <define name="anyXHTML"> + <element> + <nsName ns="http://www.w3.org/1999/xhtml"/> + <zeroOrMore> + <choice> + <attribute> + <anyName/> + </attribute> + <text/> + <ref name="anyXHTML"/> + </choice> + </zeroOrMore> + </element> + </define> + <define name="xhtmlDiv"> + <element name="xhtml:div"> + <zeroOrMore> + <choice> + <attribute> + <anyName/> + </attribute> + <text/> + <ref name="anyXHTML"/> + </choice> + </zeroOrMore> + </element> + </define> +</grammar> diff --git a/nova/api/openstack/schemas/v1.1/extension.rng b/nova/api/openstack/schemas/v1.1/extension.rng new file mode 100644 index 000000000..336659755 --- /dev/null +++ b/nova/api/openstack/schemas/v1.1/extension.rng @@ -0,0 +1,11 @@ +<element name="extension" ns="http://docs.openstack.org/compute/api/v1.1" + xmlns="http://relaxng.org/ns/structure/1.0"> + <attribute name="alias"> <text/> </attribute> + <attribute name="name"> <text/> </attribute> + <attribute name="namespace"> <text/> </attribute> + <attribute name="updated"> <text/> </attribute> + <element name="description"> <text/> </element> + <zeroOrMore> + <externalRef href="../atom-link.rng"/> + </zeroOrMore> +</element> diff --git a/nova/api/openstack/schemas/v1.1/extensions.rng b/nova/api/openstack/schemas/v1.1/extensions.rng new file mode 100644 index 000000000..4d8bff646 --- /dev/null +++ b/nova/api/openstack/schemas/v1.1/extensions.rng @@ -0,0 +1,6 @@ +<element name="extensions" xmlns="http://relaxng.org/ns/structure/1.0" + ns="http://docs.openstack.org/compute/api/v1.1"> + <zeroOrMore> + <externalRef href="extension.rng"/> + </zeroOrMore> +</element> diff --git a/nova/api/openstack/server_metadata.py b/nova/api/openstack/server_metadata.py index b0b014f86..2b235f79a 100644 --- a/nova/api/openstack/server_metadata.py +++ b/nova/api/openstack/server_metadata.py @@ -57,18 +57,12 @@ class Controller(object): context = req.environ['nova.context'] - try: - self.compute_api.update_or_create_instance_metadata(context, - server_id, - metadata) - except exception.InstanceNotFound: - msg = _('Server does not exist') - raise exc.HTTPNotFound(explanation=msg) + new_metadata = self._update_instance_metadata(context, + server_id, + metadata, + delete=False) - except quota.QuotaError as error: - self._handle_quota_error(error) - - return body + return {'metadata': new_metadata} def update(self, req, server_id, id, body): try: @@ -78,19 +72,22 @@ class Controller(object): raise exc.HTTPBadRequest(explanation=expl) try: - meta_value = meta_item.pop(id) + meta_value = meta_item[id] except (AttributeError, KeyError): expl = _('Request body and URI mismatch') raise exc.HTTPBadRequest(explanation=expl) - if len(meta_item) > 0: + if len(meta_item) > 1: expl = _('Request body contains too many items') raise exc.HTTPBadRequest(explanation=expl) context = req.environ['nova.context'] - self._set_instance_metadata(context, server_id, meta_item) + self._update_instance_metadata(context, + server_id, + meta_item, + delete=False) - return {'meta': {id: meta_value}} + return {'meta': meta_item} def update_all(self, req, server_id, body): try: @@ -100,20 +97,26 @@ class Controller(object): raise exc.HTTPBadRequest(explanation=expl) context = req.environ['nova.context'] - self._set_instance_metadata(context, server_id, metadata) + new_metadata = self._update_instance_metadata(context, + server_id, + metadata, + delete=True) - return {'metadata': metadata} + return {'metadata': new_metadata} - def _set_instance_metadata(self, context, server_id, metadata): + def _update_instance_metadata(self, context, server_id, metadata, + delete=False): try: - self.compute_api.update_or_create_instance_metadata(context, - server_id, - metadata) + return self.compute_api.update_instance_metadata(context, + server_id, + metadata, + delete) + except exception.InstanceNotFound: msg = _('Server does not exist') raise exc.HTTPNotFound(explanation=msg) - except ValueError: + except (ValueError, AttributeError): msg = _("Malformed request body") raise exc.HTTPBadRequest(explanation=msg) @@ -138,12 +141,12 @@ class Controller(object): metadata = self._get_metadata(context, server_id) try: - meta_key = metadata[id] + meta_value = metadata[id] except KeyError: msg = _("Metadata item was not found") raise exc.HTTPNotFound(explanation=msg) - self.compute_api.delete_instance_metadata(context, server_id, meta_key) + self.compute_api.delete_instance_metadata(context, server_id, id) def _handle_quota_error(self, error): """Reraise quota errors as api-specific http exceptions.""" diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index d7c4e3018..77a304941 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -44,7 +44,7 @@ FLAGS = flags.FLAGS class Controller(object): - """ The Server API controller for the OpenStack API """ + """ The Server API base controller class for the OpenStack API """ def __init__(self): self.compute_api = compute.API() @@ -53,17 +53,21 @@ class Controller(object): def index(self, req): """ Returns a list of server names and ids for a given user """ try: - servers = self._items(req, is_detail=False) + servers = self._get_servers(req, is_detail=False) except exception.Invalid as err: return exc.HTTPBadRequest(explanation=str(err)) + except exception.NotFound: + return exc.HTTPNotFound() return servers def detail(self, req): """ Returns a list of server details for a given user """ try: - servers = self._items(req, is_detail=True) + servers = self._get_servers(req, is_detail=True) except exception.Invalid as err: return exc.HTTPBadRequest(explanation=str(err)) + except exception.NotFound as err: + return exc.HTTPNotFound() return servers def _build_view(self, req, instance, is_detail=False): @@ -75,22 +79,55 @@ class Controller(object): def _action_rebuild(self, info, request, instance_id): raise NotImplementedError() - def _items(self, req, is_detail): - """Returns a list of servers for a given user. - - builder - the response model builder + def _get_servers(self, req, is_detail): + """Returns a list of servers, taking into account any search + options specified. """ - query_str = req.str_GET - reservation_id = query_str.get('reservation_id') - project_id = query_str.get('project_id') - fixed_ip = query_str.get('fixed_ip') - recurse_zones = utils.bool_from_str(query_str.get('recurse_zones')) + + search_opts = {} + search_opts.update(req.str_GET) + + context = req.environ['nova.context'] + remove_invalid_options(context, search_opts, + self._get_server_search_options()) + + # Convert recurse_zones into a boolean + search_opts['recurse_zones'] = utils.bool_from_str( + search_opts.get('recurse_zones', False)) + + # If search by 'status', we need to convert it to 'state' + # If the status is unknown, bail. + # Leave 'state' in search_opts so compute can pass it on to + # child zones.. + if 'status' in search_opts: + status = search_opts['status'] + search_opts['state'] = common.power_states_from_status(status) + if len(search_opts['state']) == 0: + reason = _('Invalid server status: %(status)s') % locals() + LOG.error(reason) + raise exception.InvalidInput(reason=reason) + + # By default, compute's get_all() will return deleted instances. + # If an admin hasn't specified a 'deleted' search option, we need + # to filter out deleted instances by setting the filter ourselves. + # ... Unless 'changes-since' is specified, because 'changes-since' + # should return recently deleted images according to the API spec. + + if 'deleted' not in search_opts: + # Admin hasn't specified deleted filter + if 'changes-since' not in search_opts: + # No 'changes-since', so we need to find non-deleted servers + search_opts['deleted'] = False + else: + # This is the default, but just in case.. + search_opts['deleted'] = True + instance_list = self.compute_api.get_all( - req.environ['nova.context'], - reservation_id=reservation_id, - project_id=project_id, - fixed_ip=fixed_ip, - recurse_zones=recurse_zones) + context, search_opts=search_opts) + + # FIXME(comstud): 'changes-since' is not fully implemented. Where + # should this be filtered? + limited_list = self._limit_items(instance_list, req) servers = [self._build_view(req, inst, is_detail)['server'] for inst in limited_list] @@ -218,13 +255,14 @@ class Controller(object): props = {'instance_ref': server_ref} metadata = entity.get('metadata', {}) + context = req.environ["nova.context"] + common.check_img_metadata_quota_limit(context, metadata) try: props.update(metadata) except ValueError: msg = _("Invalid metadata") raise webob.exc.HTTPBadRequest(explanation=msg) - context = req.environ["nova.context"] image = self.compute_api.backup(context, instance_id, image_name, @@ -505,6 +543,7 @@ class Controller(object): class ControllerV10(Controller): + """v1.0 OpenStack API controller""" @scheduler_api.redirect_handler def delete(self, req, id): @@ -567,8 +606,13 @@ class ControllerV10(Controller): """ Determine the admin password for a server on creation """ return self.helper._get_server_admin_password_old_style(server) + def _get_server_search_options(self): + """Return server search options allowed by non-admin""" + return 'reservation_id', 'fixed_ip', 'name', 'recurse_zones' + class ControllerV11(Controller): + """v1.1 OpenStack API controller""" @scheduler_api.redirect_handler def delete(self, req, id): @@ -713,13 +757,14 @@ class ControllerV11(Controller): props = {'instance_ref': server_ref} metadata = entity.get('metadata', {}) + context = req.environ['nova.context'] + common.check_img_metadata_quota_limit(context, metadata) try: props.update(metadata) except ValueError: msg = _("Invalid metadata") raise webob.exc.HTTPBadRequest(explanation=msg) - context = req.environ['nova.context'] image = self.compute_api.snapshot(context, instance_id, image_name, @@ -740,9 +785,17 @@ class ControllerV11(Controller): """ Determine the admin password for a server on creation """ return self.helper._get_server_admin_password_new_style(server) + def _get_server_search_options(self): + """Return server search options allowed by non-admin""" + return ('reservation_id', 'name', 'recurse_zones', + 'status', 'image', 'flavor', 'changes-since') + class HeadersSerializer(wsgi.ResponseHeadersSerializer): + def create(self, response, data): + response.status_int = 202 + def delete(self, response, data): response.status_int = 204 @@ -920,3 +973,18 @@ def _get_metadata(): }, } return metadata + + +def remove_invalid_options(context, search_options, allowed_search_options): + """Remove search options that are not valid for non-admin API/context""" + if FLAGS.allow_admin_api and context.is_admin: + # Allow all options + return + # Otherwise, strip out all unknown options + unknown_options = [opt for opt in search_options + if opt not in allowed_search_options] + unk_opt_str = ", ".join(unknown_options) + log_msg = _("Removing options '%(unk_opt_str)s' from query") % locals() + LOG.debug(log_msg) + for opt in unknown_options: + search_options.pop(opt, None) diff --git a/nova/api/openstack/views/images.py b/nova/api/openstack/views/images.py index 873ce212a..912303d14 100644 --- a/nova/api/openstack/views/images.py +++ b/nova/api/openstack/views/images.py @@ -77,7 +77,9 @@ class ViewBuilder(object): "status": image_obj.get("status"), }) - if image["status"] == "SAVING": + if image["status"].upper() == "ACTIVE": + image["progress"] = 100 + else: image["progress"] = 0 return image diff --git a/nova/api/openstack/views/servers.py b/nova/api/openstack/views/servers.py index 2873a8e0f..8222f6766 100644 --- a/nova/api/openstack/views/servers.py +++ b/nova/api/openstack/views/servers.py @@ -20,7 +20,6 @@ import hashlib import os from nova import exception -from nova.compute import power_state import nova.compute import nova.context from nova.api.openstack import common @@ -61,24 +60,11 @@ class ViewBuilder(object): def _build_detail(self, inst): """Returns a detailed model of a server.""" - power_mapping = { - None: 'BUILD', - power_state.NOSTATE: 'BUILD', - power_state.RUNNING: 'ACTIVE', - power_state.BLOCKED: 'ACTIVE', - power_state.SUSPENDED: 'SUSPENDED', - power_state.PAUSED: 'PAUSED', - power_state.SHUTDOWN: 'SHUTDOWN', - power_state.SHUTOFF: 'SHUTOFF', - power_state.CRASHED: 'ERROR', - power_state.FAILED: 'ERROR', - power_state.BUILDING: 'BUILD', - } inst_dict = { 'id': inst['id'], 'name': inst['display_name'], - 'status': power_mapping[inst.get('state')]} + 'status': common.status_from_power_state(inst.get('state'))} ctxt = nova.context.get_admin_context() compute_api = nova.compute.API() diff --git a/nova/api/openstack/xmlutil.py b/nova/api/openstack/xmlutil.py new file mode 100644 index 000000000..97ad90ada --- /dev/null +++ b/nova/api/openstack/xmlutil.py @@ -0,0 +1,37 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 OpenStack LLC. +# 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 os.path + +from lxml import etree + +from nova import utils + + +XMLNS_V10 = 'http://docs.rackspacecloud.com/servers/api/v1.0' +XMLNS_V11 = 'http://docs.openstack.org/compute/api/v1.1' +XMLNS_ATOM = 'http://www.w3.org/2005/Atom' + + +def validate_schema(xml, schema_name): + if type(xml) is str: + xml = etree.fromstring(xml) + schema_path = os.path.join(utils.novadir(), + 'nova/api/openstack/schemas/v1.1/%s.rng' % schema_name) + schema_doc = etree.parse(schema_path) + relaxng = etree.RelaxNG(schema_doc) + relaxng.assertValid(xml) diff --git a/nova/api/openstack/zones.py b/nova/api/openstack/zones.py index f7fd87bcd..a2bf267ed 100644 --- a/nova/api/openstack/zones.py +++ b/nova/api/openstack/zones.py @@ -166,7 +166,7 @@ class Controller(object): return self.helper._get_server_admin_password_old_style(server) -class ControllerV11(object): +class ControllerV11(Controller): """Controller for 1.1 Zone resources.""" def _get_server_admin_password(self, server): |
