summaryrefslogtreecommitdiffstats
path: root/nova/api
diff options
context:
space:
mode:
authorRyu Ishimoto <ryu@midokura.jp>2011-08-16 14:20:09 +0900
committerRyu Ishimoto <ryu@midokura.jp>2011-08-16 14:20:09 +0900
commit9d6b9c01a5652cea1aa51aa56eafada92fa82f7a (patch)
tree82e2f37e43dcc3c2eeca0da24326fa3116eae5ea /nova/api
parent7407a1a86c4039bdc541e9a26cc68c9c93f49bc3 (diff)
parentea53d0f37a4f478ffbe18516f99ca26192117e80 (diff)
downloadnova-9d6b9c01a5652cea1aa51aa56eafada92fa82f7a.tar.gz
nova-9d6b9c01a5652cea1aa51aa56eafada92fa82f7a.tar.xz
nova-9d6b9c01a5652cea1aa51aa56eafada92fa82f7a.zip
Merged trunk
Diffstat (limited to 'nova/api')
-rw-r--r--nova/api/direct.py1
-rw-r--r--nova/api/ec2/__init__.py8
-rw-r--r--nova/api/ec2/cloud.py243
-rw-r--r--nova/api/ec2/ec2utils.py29
-rw-r--r--nova/api/openstack/__init__.py12
-rw-r--r--nova/api/openstack/common.py75
-rw-r--r--nova/api/openstack/contrib/admin_only.py30
-rw-r--r--nova/api/openstack/contrib/floating_ips.py17
-rw-r--r--nova/api/openstack/contrib/hosts.py32
-rw-r--r--nova/api/openstack/contrib/keypairs.py145
-rw-r--r--nova/api/openstack/contrib/security_groups.py466
-rw-r--r--nova/api/openstack/create_instance_helper.py123
-rw-r--r--nova/api/openstack/extensions.py46
-rw-r--r--nova/api/openstack/image_metadata.py16
-rw-r--r--nova/api/openstack/images.py5
-rw-r--r--nova/api/openstack/schemas/atom-link.rng141
-rw-r--r--nova/api/openstack/schemas/atom.rng597
-rw-r--r--nova/api/openstack/schemas/v1.1/extension.rng11
-rw-r--r--nova/api/openstack/schemas/v1.1/extensions.rng6
-rw-r--r--nova/api/openstack/server_metadata.py51
-rw-r--r--nova/api/openstack/servers.py159
-rw-r--r--nova/api/openstack/views/images.py4
-rw-r--r--nova/api/openstack/views/servers.py16
-rw-r--r--nova/api/openstack/xmlutil.py37
-rw-r--r--nova/api/openstack/zones.py2
25 files changed, 2068 insertions, 204 deletions
diff --git a/nova/api/direct.py b/nova/api/direct.py
index 139c46d63..fdd2943d2 100644
--- a/nova/api/direct.py
+++ b/nova/api/direct.py
@@ -48,6 +48,7 @@ import nova.api.openstack.wsgi
# Global storage for registering modules.
ROUTES = {}
+
def register_service(path, handle):
"""Register a service handle at a given path.
diff --git a/nova/api/ec2/__init__.py b/nova/api/ec2/__init__.py
index 804e54ef9..96df97393 100644
--- a/nova/api/ec2/__init__.py
+++ b/nova/api/ec2/__init__.py
@@ -354,6 +354,14 @@ 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.InvalidPortRange as ex:
+ LOG.debug(_('InvalidPortRange 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/__init__.py b/nova/api/openstack/__init__.py
index d6a98c2cd..e0c1e9d04 100644
--- a/nova/api/openstack/__init__.py
+++ b/nova/api/openstack/__init__.py
@@ -50,6 +50,9 @@ FLAGS = flags.FLAGS
flags.DEFINE_bool('allow_admin_api',
False,
'When True, this API service will accept admin operations.')
+flags.DEFINE_bool('allow_instance_snapshots',
+ True,
+ 'When True, this API service will permit instance snapshot operations.')
class FaultWrapper(base_wsgi.Middleware):
@@ -82,7 +85,10 @@ class APIRouter(base_wsgi.Router):
self._setup_routes(mapper)
super(APIRouter, self).__init__(mapper)
- def _setup_routes(self, mapper, version):
+ def _setup_routes(self, mapper):
+ raise NotImplementedError(_("You must implement _setup_routes."))
+
+ def _setup_base_routes(self, mapper, version):
"""Routes common to all versions."""
server_members = self.server_members
@@ -153,7 +159,7 @@ class APIRouterV10(APIRouter):
"""Define routes specific to OpenStack API V1.0."""
def _setup_routes(self, mapper):
- super(APIRouterV10, self)._setup_routes(mapper, '1.0')
+ self._setup_base_routes(mapper, '1.0')
mapper.resource("shared_ip_group", "shared_ip_groups",
collection={'detail': 'GET'},
@@ -169,7 +175,7 @@ class APIRouterV11(APIRouter):
"""Define routes specific to OpenStack API V1.1."""
def _setup_routes(self, mapper):
- super(APIRouterV11, self)._setup_routes(mapper, '1.1')
+ self._setup_base_routes(mapper, '1.1')
image_metadata_controller = image_metadata.create_resource()
diff --git a/nova/api/openstack/common.py b/nova/api/openstack/common.py
index 715b9e4a4..b2a675653 100644
--- a/nova/api/openstack/common.py
+++ b/nova/api/openstack/common.py
@@ -15,6 +15,7 @@
# License for the specific language governing permissions and limitations
# under the License.
+import functools
import re
import urlparse
from xml.dom import minidom
@@ -24,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')
@@ -35,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.
@@ -134,13 +169,20 @@ def get_id_from_href(href):
Returns: 123
"""
- if re.match(r'\d+$', str(href)):
+ LOG.debug(_("Attempting to treat %(href)s as an integer ID.") % locals())
+
+ try:
return int(href)
+ except ValueError:
+ pass
+
+ LOG.debug(_("Attempting to treat %(href)s as a URL.") % locals())
+
try:
return int(urlparse.urlsplit(href).path.split('/')[-1])
- except ValueError, e:
- LOG.debug(_("Error extracting id from href: %s") % href)
- raise ValueError(_('could not parse id from href'))
+ except ValueError as error:
+ LOG.debug(_("Failed to parse ID from %(href)s: %(error)s") % locals())
+ raise
def remove_version_from_href(href):
@@ -154,7 +196,8 @@ 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)
+ new_path = re.sub(r'^/v[0-9]+\.[0-9]+(/|$)', r'\1', parsed_url.path,
+ count=1)
if new_path == parsed_url.path:
msg = _('href %s does not contain version') % href
@@ -191,6 +234,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):
@@ -279,3 +332,15 @@ class MetadataXMLSerializer(wsgi.XMLDictSerializer):
def default(self, *args, **kwargs):
return ''
+
+
+def check_snapshots_enabled(f):
+ @functools.wraps(f)
+ def inner(*args, **kwargs):
+ if not FLAGS.allow_instance_snapshots:
+ LOG.warn(_('Rejecting snapshot request, snapshots currently'
+ ' disabled'))
+ msg = _("Instance snapshots are not permitted at this time.")
+ raise webob.exc.HTTPBadRequest(explanation=msg)
+ return f(*args, **kwargs)
+ return inner
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..44b35c385 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']}
@@ -39,8 +43,8 @@ def _translate_floating_ip_view(floating_ip):
def _translate_floating_ips_view(floating_ips):
- return {'floating_ips': [_translate_floating_ip_view(floating_ip)
- for floating_ip in floating_ips]}
+ return {'floating_ips': [_translate_floating_ip_view(ip)['floating_ip']
+ for ip in floating_ips]}
class FloatingIPController(object):
@@ -97,9 +101,12 @@ class FloatingIPController(object):
def delete(self, req, id):
context = req.environ['nova.context']
-
ip = self.network_api.get_floating_ip(context, id)
- self.network_api.release_floating_ip(context, address=ip)
+
+ if 'fixed_ip' in ip:
+ self.disassociate(req, id)
+
+ self.network_api.release_floating_ip(context, address=ip['address'])
return {'released': {
"id": ip['id'],
@@ -124,7 +131,7 @@ class FloatingIPController(object):
"floating_ip": floating_ip,
"fixed_ip": fixed_ip}}
- def disassociate(self, req, id):
+ def disassociate(self, req, id, body=None):
""" POST /floating_ips/{id}/disassociate """
context = req.environ['nova.context']
floating_ip = self.network_api.get_floating_ip(context, id)
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/contrib/keypairs.py b/nova/api/openstack/contrib/keypairs.py
new file mode 100644
index 000000000..201648ab5
--- /dev/null
+++ b/nova/api/openstack/contrib/keypairs.py
@@ -0,0 +1,145 @@
+# 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.
+
+""" Keypair management extension"""
+
+import os
+import shutil
+import tempfile
+
+from webob import exc
+
+from nova import crypto
+from nova import db
+from nova import exception
+from nova.api.openstack import extensions
+
+
+class KeypairController(object):
+ """ Keypair API controller for the Openstack API """
+
+ # TODO(ja): both this file and nova.api.ec2.cloud.py have similar logic.
+ # move the common keypair logic to nova.compute.API?
+
+ def _gen_key(self):
+ """
+ Generate a key
+ """
+ private_key, public_key, fingerprint = crypto.generate_key_pair()
+ return {'private_key': private_key,
+ 'public_key': public_key,
+ 'fingerprint': fingerprint}
+
+ def create(self, req, body):
+ """
+ Create or import keypair.
+
+ Sending name will generate a key and return private_key
+ and fingerprint.
+
+ You can send a public_key to add an existing ssh key
+
+ params: keypair object with:
+ name (required) - string
+ public_key (optional) - string
+ """
+
+ context = req.environ['nova.context']
+ params = body['keypair']
+ name = params['name']
+
+ # NOTE(ja): generation is slow, so shortcut invalid name exception
+ try:
+ db.key_pair_get(context, context.user_id, name)
+ raise exception.KeyPairExists(key_name=name)
+ except exception.NotFound:
+ pass
+
+ keypair = {'user_id': context.user_id,
+ 'name': name}
+
+ # import if public_key is sent
+ if 'public_key' in params:
+ tmpdir = tempfile.mkdtemp()
+ fn = os.path.join(tmpdir, 'import.pub')
+ with open(fn, 'w') as pub:
+ pub.write(params['public_key'])
+ fingerprint = crypto.generate_fingerprint(fn)
+ shutil.rmtree(tmpdir)
+ keypair['public_key'] = params['public_key']
+ keypair['fingerprint'] = fingerprint
+ else:
+ generated_key = self._gen_key()
+ keypair['private_key'] = generated_key['private_key']
+ keypair['public_key'] = generated_key['public_key']
+ keypair['fingerprint'] = generated_key['fingerprint']
+
+ db.key_pair_create(context, keypair)
+ return {'keypair': keypair}
+
+ def delete(self, req, id):
+ """
+ Delete a keypair with a given name
+ """
+ context = req.environ['nova.context']
+ db.key_pair_destroy(context, context.user_id, id)
+ return exc.HTTPAccepted()
+
+ def index(self, req):
+ """
+ List of keypairs for a user
+ """
+ context = req.environ['nova.context']
+ key_pairs = db.key_pair_get_all_by_user(context, context.user_id)
+ rval = []
+ for key_pair in key_pairs:
+ rval.append({'keypair': {
+ 'name': key_pair['name'],
+ 'public_key': key_pair['public_key'],
+ 'fingerprint': key_pair['fingerprint'],
+ }})
+
+ return {'keypairs': rval}
+
+
+class Keypairs(extensions.ExtensionDescriptor):
+
+ def get_name(self):
+ return "Keypairs"
+
+ def get_alias(self):
+ return "os-keypairs"
+
+ def get_description(self):
+ return "Keypair Support"
+
+ def get_namespace(self):
+ return \
+ "http://docs.openstack.org/ext/keypairs/api/v1.1"
+
+ def get_updated(self):
+ return "2011-08-08T00:00:00+00:00"
+
+ def get_resources(self):
+ resources = []
+
+ res = extensions.ResourceExtension(
+ 'os-keypairs',
+ KeypairController())
+
+ resources.append(res)
+ return resources
diff --git a/nova/api/openstack/contrib/security_groups.py b/nova/api/openstack/contrib/security_groups.py
new file mode 100644
index 000000000..6c57fbb51
--- /dev/null
+++ b/nova/api/openstack/contrib/security_groups.py
@@ -0,0 +1,466 @@
+# 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.
+
+"""The security groups extension."""
+
+import netaddr
+import urllib
+from webob import exc
+import webob
+
+from nova import compute
+from nova import db
+from nova import exception
+from nova import flags
+from nova import log as logging
+from nova.api.openstack import common
+from nova.api.openstack import extensions
+from nova.api.openstack import wsgi
+
+
+from xml.dom import minidom
+
+
+LOG = logging.getLogger("nova.api.contrib.security_groups")
+FLAGS = flags.FLAGS
+
+
+class SecurityGroupController(object):
+ """The Security group API controller for the OpenStack API."""
+
+ def __init__(self):
+ self.compute_api = compute.API()
+ super(SecurityGroupController, self).__init__()
+
+ def _format_security_group_rule(self, context, rule):
+ sg_rule = {}
+ sg_rule['id'] = rule.id
+ sg_rule['parent_group_id'] = rule.parent_group_id
+ sg_rule['ip_protocol'] = rule.protocol
+ sg_rule['from_port'] = rule.from_port
+ sg_rule['to_port'] = rule.to_port
+ sg_rule['group'] = {}
+ sg_rule['ip_range'] = {}
+ if rule.group_id:
+ source_group = db.security_group_get(context, rule.group_id)
+ sg_rule['group'] = {'name': source_group.name,
+ 'tenant_id': source_group.project_id}
+ else:
+ sg_rule['ip_range'] = {'cidr': rule.cidr}
+ return sg_rule
+
+ def _format_security_group(self, context, group):
+ security_group = {}
+ security_group['id'] = group.id
+ security_group['description'] = group.description
+ security_group['name'] = group.name
+ security_group['tenant_id'] = group.project_id
+ security_group['rules'] = []
+ for rule in group.rules:
+ security_group['rules'] += [self._format_security_group_rule(
+ context, rule)]
+ return security_group
+
+ def show(self, req, id):
+ """Return data about the given security group."""
+ context = req.environ['nova.context']
+ try:
+ id = int(id)
+ security_group = db.security_group_get(context, id)
+ except ValueError:
+ msg = _("Security group id is not integer")
+ return exc.HTTPBadRequest(explanation=msg)
+ except exception.NotFound as exp:
+ return exc.HTTPNotFound(explanation=unicode(exp))
+
+ return {'security_group': self._format_security_group(context,
+ security_group)}
+
+ def delete(self, req, id):
+ """Delete a security group."""
+ context = req.environ['nova.context']
+ try:
+ id = int(id)
+ security_group = db.security_group_get(context, id)
+ except ValueError:
+ msg = _("Security group id is not integer")
+ return exc.HTTPBadRequest(explanation=msg)
+ except exception.SecurityGroupNotFound as exp:
+ return exc.HTTPNotFound(explanation=unicode(exp))
+
+ LOG.audit(_("Delete security group %s"), id, context=context)
+ db.security_group_destroy(context, security_group.id)
+
+ return exc.HTTPAccepted()
+
+ def index(self, req):
+ """Returns a list of security groups"""
+ context = req.environ['nova.context']
+
+ self.compute_api.ensure_default_security_group(context)
+ groups = db.security_group_get_by_project(context,
+ context.project_id)
+ limited_list = common.limited(groups, req)
+ result = [self._format_security_group(context, group)
+ for group in limited_list]
+
+ return {'security_groups':
+ list(sorted(result,
+ key=lambda k: (k['tenant_id'], k['name'])))}
+
+ def create(self, req, body):
+ """Creates a new security group."""
+ context = req.environ['nova.context']
+ if not body:
+ return exc.HTTPUnprocessableEntity()
+
+ security_group = body.get('security_group', None)
+
+ if security_group is None:
+ return exc.HTTPUnprocessableEntity()
+
+ group_name = security_group.get('name', None)
+ group_description = security_group.get('description', None)
+
+ self._validate_security_group_property(group_name, "name")
+ self._validate_security_group_property(group_description,
+ "description")
+ group_name = group_name.strip()
+ group_description = group_description.strip()
+
+ 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):
+ msg = _('Security group %s already exists') % group_name
+ raise exc.HTTPBadRequest(explanation=msg)
+
+ group = {'user_id': context.user_id,
+ 'project_id': context.project_id,
+ 'name': group_name,
+ 'description': group_description}
+ group_ref = db.security_group_create(context, group)
+
+ return {'security_group': self._format_security_group(context,
+ group_ref)}
+
+ def _validate_security_group_property(self, value, typ):
+ """ typ will be either 'name' or 'description',
+ depending on the caller
+ """
+ try:
+ val = value.strip()
+ except AttributeError:
+ msg = _("Security group %s is not a string or unicode") % typ
+ raise exc.HTTPBadRequest(explanation=msg)
+ if not val:
+ msg = _("Security group %s cannot be empty.") % typ
+ raise exc.HTTPBadRequest(explanation=msg)
+ if len(val) > 255:
+ msg = _("Security group %s should not be greater "
+ "than 255 characters.") % typ
+ raise exc.HTTPBadRequest(explanation=msg)
+
+
+class SecurityGroupRulesController(SecurityGroupController):
+
+ def create(self, req, body):
+ context = req.environ['nova.context']
+
+ if not body:
+ raise exc.HTTPUnprocessableEntity()
+
+ if not 'security_group_rule' in body:
+ raise exc.HTTPUnprocessableEntity()
+
+ self.compute_api.ensure_default_security_group(context)
+
+ sg_rule = body['security_group_rule']
+ parent_group_id = sg_rule.get('parent_group_id', None)
+ try:
+ parent_group_id = int(parent_group_id)
+ security_group = db.security_group_get(context, parent_group_id)
+ except ValueError:
+ msg = _("Parent group id is not integer")
+ return exc.HTTPBadRequest(explanation=msg)
+ except exception.NotFound as exp:
+ msg = _("Security group (%s) not found") % parent_group_id
+ return exc.HTTPNotFound(explanation=msg)
+
+ msg = _("Authorize security group ingress %s")
+ LOG.audit(msg, security_group['name'], context=context)
+
+ try:
+ values = self._rule_args_to_dict(context,
+ to_port=sg_rule.get('to_port'),
+ from_port=sg_rule.get('from_port'),
+ parent_group_id=sg_rule.get('parent_group_id'),
+ ip_protocol=sg_rule.get('ip_protocol'),
+ cidr=sg_rule.get('cidr'),
+ group_id=sg_rule.get('group_id'))
+ except Exception as exp:
+ raise exc.HTTPBadRequest(explanation=unicode(exp))
+
+ if values is None:
+ msg = _("Not enough parameters to build a "
+ "valid rule.")
+ raise exc.HTTPBadRequest(explanation=msg)
+
+ values['parent_group_id'] = security_group.id
+
+ if self._security_group_rule_exists(security_group, values):
+ msg = _('This rule already exists in group %s') % parent_group_id
+ raise exc.HTTPBadRequest(explanation=msg)
+
+ security_group_rule = db.security_group_rule_create(context, values)
+
+ self.compute_api.trigger_security_group_rules_refresh(context,
+ security_group_id=security_group['id'])
+
+ return {'security_group_rule': self._format_security_group_rule(
+ context,
+ security_group_rule)}
+
+ def _security_group_rule_exists(self, security_group, values):
+ """Indicates whether the specified rule values are already
+ defined in the given security group.
+ """
+ for rule in security_group.rules:
+ if 'group_id' in values:
+ if rule['group_id'] == values['group_id']:
+ return True
+ else:
+ is_duplicate = True
+ for key in ('cidr', 'from_port', 'to_port', 'protocol'):
+ if rule[key] != values[key]:
+ is_duplicate = False
+ break
+ if is_duplicate:
+ return True
+ return False
+
+ def _rule_args_to_dict(self, context, to_port=None, from_port=None,
+ parent_group_id=None, ip_protocol=None,
+ cidr=None, group_id=None):
+ values = {}
+
+ if group_id:
+ try:
+ parent_group_id = int(parent_group_id)
+ group_id = int(group_id)
+ except ValueError:
+ msg = _("Parent or group id is not integer")
+ raise exception.InvalidInput(reason=msg)
+
+ if parent_group_id == group_id:
+ msg = _("Parent group id and group id cannot be same")
+ raise exception.InvalidInput(reason=msg)
+
+ values['group_id'] = group_id
+ #check if groupId exists
+ db.security_group_get(context, group_id)
+ elif cidr:
+ # If this fails, it throws an exception. This is what we want.
+ try:
+ cidr = urllib.unquote(cidr).decode()
+ netaddr.IPNetwork(cidr)
+ except Exception:
+ raise exception.InvalidCidr(cidr=cidr)
+ values['cidr'] = cidr
+ else:
+ values['cidr'] = '0.0.0.0/0'
+
+ if ip_protocol and from_port and to_port:
+
+ try:
+ from_port = int(from_port)
+ to_port = int(to_port)
+ except ValueError:
+ raise exception.InvalidPortRange(from_port=from_port,
+ to_port=to_port)
+ ip_protocol = str(ip_protocol)
+ if ip_protocol.upper() not in ['TCP', 'UDP', 'ICMP']:
+ raise exception.InvalidIpProtocol(protocol=ip_protocol)
+ if ((min(from_port, to_port) < -1) or
+ (max(from_port, to_port) > 65535)):
+ raise exception.InvalidPortRange(from_port=from_port,
+ to_port=to_port)
+
+ values['protocol'] = ip_protocol
+ values['from_port'] = from_port
+ values['to_port'] = to_port
+ else:
+ # If cidr based filtering, protocol and ports are mandatory
+ if 'cidr' in values:
+ return None
+
+ return values
+
+ def delete(self, req, id):
+ context = req.environ['nova.context']
+
+ self.compute_api.ensure_default_security_group(context)
+ try:
+ id = int(id)
+ rule = db.security_group_rule_get(context, id)
+ except ValueError:
+ msg = _("Rule id is not integer")
+ return exc.HTTPBadRequest(explanation=msg)
+ except exception.NotFound as exp:
+ msg = _("Rule (%s) not found") % id
+ return exc.HTTPNotFound(explanation=msg)
+
+ group_id = rule.parent_group_id
+ self.compute_api.ensure_default_security_group(context)
+ security_group = db.security_group_get(context, group_id)
+
+ msg = _("Revoke security group ingress %s")
+ LOG.audit(msg, security_group['name'], context=context)
+
+ db.security_group_rule_destroy(context, rule['id'])
+ self.compute_api.trigger_security_group_rules_refresh(context,
+ security_group_id=security_group['id'])
+
+ return exc.HTTPAccepted()
+
+
+class Security_groups(extensions.ExtensionDescriptor):
+ def get_name(self):
+ return "SecurityGroups"
+
+ def get_alias(self):
+ return "security_groups"
+
+ def get_description(self):
+ return "Security group support"
+
+ def get_namespace(self):
+ return "http://docs.openstack.org/ext/securitygroups/api/v1.1"
+
+ def get_updated(self):
+ return "2011-07-21T00:00:00+00:00"
+
+ def get_resources(self):
+ resources = []
+
+ metadata = _get_metadata()
+ body_serializers = {
+ 'application/xml': wsgi.XMLDictSerializer(metadata=metadata,
+ xmlns=wsgi.XMLNS_V11),
+ }
+ serializer = wsgi.ResponseSerializer(body_serializers, None)
+
+ body_deserializers = {
+ 'application/xml': SecurityGroupXMLDeserializer(),
+ }
+ deserializer = wsgi.RequestDeserializer(body_deserializers)
+
+ res = extensions.ResourceExtension('os-security-groups',
+ controller=SecurityGroupController(),
+ deserializer=deserializer,
+ serializer=serializer)
+
+ resources.append(res)
+
+ body_deserializers = {
+ 'application/xml': SecurityGroupRulesXMLDeserializer(),
+ }
+ deserializer = wsgi.RequestDeserializer(body_deserializers)
+
+ res = extensions.ResourceExtension('os-security-group-rules',
+ controller=SecurityGroupRulesController(),
+ deserializer=deserializer,
+ serializer=serializer)
+ resources.append(res)
+ return resources
+
+
+class SecurityGroupXMLDeserializer(wsgi.MetadataXMLDeserializer):
+ """
+ Deserializer to handle xml-formatted security group requests.
+ """
+ def create(self, string):
+ """Deserialize an xml-formatted security group create request"""
+ dom = minidom.parseString(string)
+ security_group = {}
+ sg_node = self.find_first_child_named(dom,
+ 'security_group')
+ if sg_node is not None:
+ if sg_node.hasAttribute('name'):
+ security_group['name'] = sg_node.getAttribute('name')
+ desc_node = self.find_first_child_named(sg_node,
+ "description")
+ if desc_node:
+ security_group['description'] = self.extract_text(desc_node)
+ return {'body': {'security_group': security_group}}
+
+
+class SecurityGroupRulesXMLDeserializer(wsgi.MetadataXMLDeserializer):
+ """
+ Deserializer to handle xml-formatted security group requests.
+ """
+
+ def create(self, string):
+ """Deserialize an xml-formatted security group create request"""
+ dom = minidom.parseString(string)
+ security_group_rule = self._extract_security_group_rule(dom)
+ return {'body': {'security_group_rule': security_group_rule}}
+
+ def _extract_security_group_rule(self, node):
+ """Marshal the security group rule attribute of a parsed request"""
+ sg_rule = {}
+ sg_rule_node = self.find_first_child_named(node,
+ 'security_group_rule')
+ if sg_rule_node is not None:
+ ip_protocol_node = self.find_first_child_named(sg_rule_node,
+ "ip_protocol")
+ if ip_protocol_node is not None:
+ sg_rule['ip_protocol'] = self.extract_text(ip_protocol_node)
+
+ from_port_node = self.find_first_child_named(sg_rule_node,
+ "from_port")
+ if from_port_node is not None:
+ sg_rule['from_port'] = self.extract_text(from_port_node)
+
+ to_port_node = self.find_first_child_named(sg_rule_node, "to_port")
+ if to_port_node is not None:
+ sg_rule['to_port'] = self.extract_text(to_port_node)
+
+ parent_group_id_node = self.find_first_child_named(sg_rule_node,
+ "parent_group_id")
+ if parent_group_id_node is not None:
+ sg_rule['parent_group_id'] = self.extract_text(
+ parent_group_id_node)
+
+ group_id_node = self.find_first_child_named(sg_rule_node,
+ "group_id")
+ if group_id_node is not None:
+ sg_rule['group_id'] = self.extract_text(group_id_node)
+
+ cidr_node = self.find_first_child_named(sg_rule_node, "cidr")
+ if cidr_node is not None:
+ sg_rule['cidr'] = self.extract_text(cidr_node)
+
+ return sg_rule
+
+
+def _get_metadata():
+ metadata = {
+ "attributes": {
+ "security_group": ["id", "tenant_id", "name"],
+ "rule": ["id", "parent_group_id"],
+ "security_group_rule": ["id", "parent_group_id"],
+ }
+ }
+ return metadata
diff --git a/nova/api/openstack/create_instance_helper.py b/nova/api/openstack/create_instance_helper.py
index 2a8e7fd7e..4e1da549e 100644
--- a/nova/api/openstack/create_instance_helper.py
+++ b/nova/api/openstack/create_instance_helper.py
@@ -14,8 +14,6 @@
# under the License.
import base64
-import re
-import webob
from webob import exc
from xml.dom import minidom
@@ -124,6 +122,7 @@ class CreateInstanceHelper(object):
raise exc.HTTPBadRequest(explanation=msg)
zone_blob = server_dict.get('blob')
+ availability_zone = server_dict.get('availability_zone')
name = server_dict['name']
self._validate_server_name(name)
name = name.strip()
@@ -163,7 +162,8 @@ class CreateInstanceHelper(object):
zone_blob=zone_blob,
reservation_id=reservation_id,
min_count=min_count,
- max_count=max_count))
+ max_count=max_count,
+ availability_zone=availability_zone))
except quota.QuotaError as error:
self._handle_quota_error(error)
except exception.ImageNotFound as error:
@@ -304,6 +304,54 @@ class ServerXMLDeserializer(wsgi.XMLDeserializer):
metadata_deserializer = common.MetadataXMLDeserializer()
+ def create(self, string):
+ """Deserialize an xml-formatted server create request"""
+ dom = minidom.parseString(string)
+ server = self._extract_server(dom)
+ return {'body': {'server': server}}
+
+ def _extract_server(self, node):
+ """Marshal the server attribute of a parsed request"""
+ server = {}
+ server_node = self.find_first_child_named(node, 'server')
+
+ attributes = ["name", "imageId", "flavorId", "adminPass"]
+ for attr in attributes:
+ if server_node.getAttribute(attr):
+ server[attr] = server_node.getAttribute(attr)
+
+ metadata_node = self.find_first_child_named(server_node, "metadata")
+ server["metadata"] = self.metadata_deserializer.extract_metadata(
+ metadata_node)
+
+ server["personality"] = self._extract_personality(server_node)
+
+ return server
+
+ def _extract_personality(self, server_node):
+ """Marshal the personality attribute of a parsed request"""
+ node = self.find_first_child_named(server_node, "personality")
+ personality = []
+ if node is not None:
+ for file_node in self.find_children_named(node, "file"):
+ item = {}
+ if file_node.hasAttribute("path"):
+ item["path"] = file_node.getAttribute("path")
+ item["contents"] = self.extract_text(file_node)
+ personality.append(item)
+ return personality
+
+
+class ServerXMLDeserializerV11(wsgi.MetadataXMLDeserializer):
+ """
+ Deserializer to handle xml-formatted server create requests.
+
+ Handles standard server attributes as well as optional metadata
+ and personality attributes
+ """
+
+ metadata_deserializer = common.MetadataXMLDeserializer()
+
def action(self, string):
dom = minidom.parseString(string)
action_node = dom.childNodes[0]
@@ -312,6 +360,12 @@ class ServerXMLDeserializer(wsgi.XMLDeserializer):
action_deserializer = {
'createImage': self._action_create_image,
'createBackup': self._action_create_backup,
+ 'changePassword': self._action_change_password,
+ 'reboot': self._action_reboot,
+ 'rebuild': self._action_rebuild,
+ 'resize': self._action_resize,
+ 'confirmResize': self._action_confirm_resize,
+ 'revertResize': self._action_revert_resize,
}.get(action_name, self.default)
action_data = action_deserializer(action_node)
@@ -325,6 +379,46 @@ class ServerXMLDeserializer(wsgi.XMLDeserializer):
attributes = ('name', 'backup_type', 'rotation')
return self._deserialize_image_action(node, attributes)
+ def _action_change_password(self, node):
+ if not node.hasAttribute("adminPass"):
+ raise AttributeError("No adminPass was specified in request")
+ return {"adminPass": node.getAttribute("adminPass")}
+
+ def _action_reboot(self, node):
+ if not node.hasAttribute("type"):
+ raise AttributeError("No reboot type was specified in request")
+ return {"type": node.getAttribute("type")}
+
+ def _action_rebuild(self, node):
+ rebuild = {}
+ if node.hasAttribute("name"):
+ rebuild['name'] = node.getAttribute("name")
+
+ metadata_node = self.find_first_child_named(node, "metadata")
+ if metadata_node is not None:
+ rebuild["metadata"] = self.extract_metadata(metadata_node)
+
+ personality = self._extract_personality(node)
+ if personality is not None:
+ rebuild["personality"] = personality
+
+ if not node.hasAttribute("imageRef"):
+ raise AttributeError("No imageRef was specified in request")
+ rebuild["imageRef"] = node.getAttribute("imageRef")
+
+ return rebuild
+
+ def _action_resize(self, node):
+ if not node.hasAttribute("flavorRef"):
+ raise AttributeError("No flavorRef was specified in request")
+ return {"flavorRef": node.getAttribute("flavorRef")}
+
+ def _action_confirm_resize(self, node):
+ return None
+
+ def _action_revert_resize(self, node):
+ return None
+
def _deserialize_image_action(self, node, allowed_attributes):
data = {}
for attribute in allowed_attributes:
@@ -332,8 +426,10 @@ class ServerXMLDeserializer(wsgi.XMLDeserializer):
if value:
data[attribute] = value
metadata_node = self.find_first_child_named(node, 'metadata')
- metadata = self.metadata_deserializer.extract_metadata(metadata_node)
- data['metadata'] = metadata
+ if metadata_node is not None:
+ metadata = self.metadata_deserializer.extract_metadata(
+ metadata_node)
+ data['metadata'] = metadata
return data
def create(self, string):
@@ -347,29 +443,32 @@ class ServerXMLDeserializer(wsgi.XMLDeserializer):
server = {}
server_node = self.find_first_child_named(node, 'server')
- attributes = ["name", "imageId", "flavorId", "imageRef",
- "flavorRef", "adminPass"]
+ attributes = ["name", "imageRef", "flavorRef", "adminPass"]
for attr in attributes:
if server_node.getAttribute(attr):
server[attr] = server_node.getAttribute(attr)
metadata_node = self.find_first_child_named(server_node, "metadata")
- server["metadata"] = self.metadata_deserializer.extract_metadata(
- metadata_node)
+ if metadata_node is not None:
+ server["metadata"] = self.extract_metadata(metadata_node)
- server["personality"] = self._extract_personality(server_node)
+ personality = self._extract_personality(server_node)
+ if personality is not None:
+ server["personality"] = personality
return server
def _extract_personality(self, server_node):
"""Marshal the personality attribute of a parsed request"""
node = self.find_first_child_named(server_node, "personality")
- personality = []
if node is not None:
+ personality = []
for file_node in self.find_children_named(node, "file"):
item = {}
if file_node.hasAttribute("path"):
item["path"] = file_node.getAttribute("path")
item["contents"] = self.extract_text(file_node)
personality.append(item)
- return personality
+ return personality
+ else:
+ return None
diff --git a/nova/api/openstack/extensions.py b/nova/api/openstack/extensions.py
index cc889703e..bb407a045 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')
@@ -265,9 +266,13 @@ class ExtensionMiddleware(base_wsgi.Middleware):
for resource in ext_mgr.get_resources():
LOG.debug(_('Extended resource: %s'),
resource.collection)
+ if resource.serializer is None:
+ resource.serializer = serializer
+
mapper.resource(resource.collection, resource.collection,
controller=wsgi.Resource(
- resource.controller, serializer=serializer),
+ resource.controller, resource.deserializer,
+ resource.serializer),
collection=resource.collection_actions,
member=resource.member_actions,
parent_resource=resource.parent)
@@ -460,46 +465,55 @@ class ResourceExtension(object):
"""Add top level resources to the OpenStack API in nova."""
def __init__(self, collection, controller, parent=None,
- collection_actions={}, member_actions={}):
+ collection_actions=None, member_actions=None,
+ deserializer=None, serializer=None):
+ if not collection_actions:
+ collection_actions = {}
+ if not member_actions:
+ member_actions = {}
self.collection = collection
self.controller = controller
self.parent = parent
self.collection_actions = collection_actions
self.member_actions = member_actions
+ self.deserializer = deserializer
+ self.serializer = serializer
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/images.py b/nova/api/openstack/images.py
index c76738d30..0aabb9e56 100644
--- a/nova/api/openstack/images.py
+++ b/nova/api/openstack/images.py
@@ -106,6 +106,7 @@ class Controller(object):
class ControllerV10(Controller):
"""Version 1.0 specific controller logic."""
+ @common.check_snapshots_enabled
def create(self, req, body):
"""Snapshot a server instance and save the image."""
try:
@@ -143,7 +144,7 @@ class ControllerV10(Controller):
"""
context = req.environ['nova.context']
filters = self._get_filters(req)
- images = self._image_service.index(context, filters)
+ images = self._image_service.index(context, filters=filters)
images = common.limited(images, req)
builder = self.get_builder(req).build
return dict(images=[builder(image, detail=False) for image in images])
@@ -156,7 +157,7 @@ class ControllerV10(Controller):
"""
context = req.environ['nova.context']
filters = self._get_filters(req)
- images = self._image_service.detail(context, filters)
+ images = self._image_service.detail(context, filters=filters)
images = common.limited(images, req)
builder = self.get_builder(req).build
return dict(images=[builder(image, detail=True) for image in images])
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 3930982dc..335ecad86 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]
@@ -126,7 +163,7 @@ class Controller(object):
@scheduler_api.redirect_handler
def update(self, req, id, body):
- """ Updates the server name or password """
+ """Update server name then pass on to version-specific controller"""
if len(req.body) == 0:
raise exc.HTTPUnprocessableEntity()
@@ -141,17 +178,15 @@ class Controller(object):
self.helper._validate_server_name(name)
update_dict['display_name'] = name.strip()
- self._parse_update(ctxt, id, body, update_dict)
-
try:
self.compute_api.update(ctxt, id, **update_dict)
except exception.NotFound:
raise exc.HTTPNotFound()
- return exc.HTTPNoContent()
+ return self._update(ctxt, req, id, body)
- def _parse_update(self, context, id, inst_dict, update_dict):
- pass
+ def _update(self, context, req, id, inst_dict):
+ return exc.HTTPNotImplemented()
@scheduler_api.redirect_handler
def action(self, req, id, body):
@@ -173,11 +208,15 @@ class Controller(object):
}
self.actions.update(admin_actions)
- for key in self.actions.keys():
- if key in body:
+ for key in body:
+ if key in self.actions:
return self.actions[key](body, req, id)
+ else:
+ msg = _("There is no such server action: %s") % (key,)
+ raise exc.HTTPBadRequest(explanation=msg)
- raise exc.HTTPNotImplemented()
+ msg = _("Invalid request body")
+ raise exc.HTTPBadRequest(explanation=msg)
def _action_create_backup(self, input_dict, req, instance_id):
"""Backup a server instance.
@@ -218,13 +257,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,
@@ -240,6 +280,7 @@ class Controller(object):
resp.headers['Location'] = image_ref
return resp
+ @common.check_snapshots_enabled
def _action_create_image(self, input_dict, req, id):
return exc.HTTPNotImplemented()
@@ -267,10 +308,16 @@ class Controller(object):
def _action_reboot(self, input_dict, req, id):
if 'reboot' in input_dict and 'type' in input_dict['reboot']:
- reboot_type = input_dict['reboot']['type']
+ valid_reboot_types = ['HARD', 'SOFT']
+ reboot_type = input_dict['reboot']['type'].upper()
+ if not valid_reboot_types.count(reboot_type):
+ msg = _("Argument 'type' for reboot is not HARD or SOFT")
+ LOG.exception(msg)
+ raise exc.HTTPBadRequest(explanation=msg)
else:
- LOG.exception(_("Missing argument 'type' for reboot"))
- raise exc.HTTPUnprocessableEntity()
+ msg = _("Missing argument 'type' for reboot")
+ LOG.exception(msg)
+ raise exc.HTTPBadRequest(explanation=msg)
try:
# TODO(gundlach): pass reboot_type, support soft reboot in
# virt driver
@@ -498,6 +545,7 @@ class Controller(object):
class ControllerV10(Controller):
+ """v1.0 OpenStack API controller"""
@scheduler_api.redirect_handler
def delete(self, req, id):
@@ -522,10 +570,11 @@ class ControllerV10(Controller):
def _limit_items(self, items, req):
return common.limited(items, req)
- def _parse_update(self, context, server_id, inst_dict, update_dict):
+ def _update(self, context, req, id, inst_dict):
if 'adminPass' in inst_dict['server']:
- self.compute_api.set_admin_password(context, server_id,
+ self.compute_api.set_admin_password(context, id,
inst_dict['server']['adminPass'])
+ return exc.HTTPNoContent()
def _action_resize(self, input_dict, req, id):
""" Resizes a given instance to the flavor size requested """
@@ -560,8 +609,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):
@@ -642,10 +696,17 @@ class ControllerV11(Controller):
LOG.info(msg)
raise exc.HTTPBadRequest(explanation=msg)
+ def _update(self, context, req, id, inst_dict):
+ instance = self.compute_api.routing_get(context, id)
+ return self._build_view(req, instance, is_detail=True)
+
def _action_resize(self, input_dict, req, id):
""" Resizes a given instance to the flavor size requested """
try:
flavor_ref = input_dict["resize"]["flavorRef"]
+ if not flavor_ref:
+ msg = _("Resize request has invalid 'flavorRef' attribute.")
+ raise exc.HTTPBadRequest(explanation=msg)
except (KeyError, TypeError):
msg = _("Resize requests require 'flavorRef' attribute.")
raise exc.HTTPBadRequest(explanation=msg)
@@ -680,6 +741,7 @@ class ControllerV11(Controller):
return webob.Response(status_int=202)
+ @common.check_snapshots_enabled
def _action_create_image(self, input_dict, req, instance_id):
"""Snapshot a server instance."""
entity = input_dict.get("createImage", {})
@@ -702,13 +764,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,
@@ -729,9 +792,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
@@ -891,11 +962,31 @@ def create_resource(version='1.0'):
'application/xml': xml_serializer,
}
+ xml_deserializer = {
+ '1.0': helper.ServerXMLDeserializer(),
+ '1.1': helper.ServerXMLDeserializerV11(),
+ }[version]
+
body_deserializers = {
- 'application/xml': helper.ServerXMLDeserializer(),
+ 'application/xml': xml_deserializer,
}
serializer = wsgi.ResponseSerializer(body_serializers, headers_serializer)
deserializer = wsgi.RequestDeserializer(body_deserializers)
return wsgi.Resource(controller, deserializer, serializer)
+
+
+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):