summaryrefslogtreecommitdiffstats
path: root/nova
diff options
context:
space:
mode:
authorDan Wendlandt <dan@nicira.com>2011-08-12 17:33:30 -0700
committerDan Wendlandt <dan@nicira.com>2011-08-12 17:33:30 -0700
commit86d2109b8f3b27e460aa41c11924166cb07d7bb4 (patch)
tree99b79011748fc0b67df507db103779237caa40a9 /nova
parent18f09f165b5dca5f11253b143045b2ff7327532d (diff)
parentfe0bde67193ce76376e72a7263b89240a63722a8 (diff)
merge in trunk, resolving conflicts with ttx's branch to switch from using sudo to run_as_root=True
Diffstat (limited to 'nova')
-rw-r--r--nova/api/direct.py4
-rw-r--r--nova/api/ec2/__init__.py8
-rw-r--r--nova/api/ec2/apirequest.py2
-rw-r--r--nova/api/ec2/cloud.py243
-rw-r--r--nova/api/ec2/ec2utils.py29
-rw-r--r--nova/api/openstack/__init__.py25
-rw-r--r--nova/api/openstack/common.py101
-rw-r--r--nova/api/openstack/contrib/admin_only.py30
-rw-r--r--nova/api/openstack/contrib/floating_ips.py12
-rw-r--r--nova/api/openstack/contrib/hosts.py32
-rw-r--r--nova/api/openstack/contrib/keypairs.py145
-rw-r--r--nova/api/openstack/create_instance_helper.py124
-rw-r--r--nova/api/openstack/extensions.py37
-rw-r--r--nova/api/openstack/image_metadata.py16
-rw-r--r--nova/api/openstack/images.py13
-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.py138
-rw-r--r--nova/api/openstack/servers.py164
-rw-r--r--nova/api/openstack/versions.py16
-rw-r--r--nova/api/openstack/views/images.py4
-rw-r--r--nova/api/openstack/views/servers.py16
-rw-r--r--nova/api/openstack/views/versions.py6
-rw-r--r--nova/api/openstack/xmlutil.py37
-rw-r--r--nova/api/openstack/zones.py2
-rw-r--r--nova/auth/novarc.template1
-rw-r--r--nova/block_device.py71
-rw-r--r--nova/cloudpipe/pipelib.py17
-rw-r--r--nova/compute/api.py220
-rw-r--r--nova/compute/manager.py94
-rw-r--r--nova/console/xvp.py1
-rw-r--r--nova/crypto.py10
-rw-r--r--nova/db/api.py32
-rw-r--r--nova/db/sqlalchemy/api.py278
-rw-r--r--nova/db/sqlalchemy/migrate_repo/versions/036_change_flavor_id_in_migrations.py72
-rw-r--r--nova/db/sqlalchemy/models.py7
-rw-r--r--nova/exception.py19
-rw-r--r--nova/flags.py10
-rw-r--r--nova/image/__init__.py3
-rw-r--r--nova/image/fake.py41
-rw-r--r--nova/image/glance.py129
-rw-r--r--nova/network/linux_net.py151
-rw-r--r--nova/objectstore/s3server.py5
-rw-r--r--nova/scheduler/api.py13
-rw-r--r--nova/scheduler/least_cost.py3
-rw-r--r--nova/scheduler/manager.py4
-rw-r--r--nova/scheduler/zone_aware_scheduler.py16
-rw-r--r--nova/scheduler/zone_manager.py9
-rw-r--r--nova/test.py43
-rw-r--r--nova/tests/api/openstack/__init__.py3
-rw-r--r--nova/tests/api/openstack/contrib/test_keypairs.py99
-rw-r--r--nova/tests/api/openstack/fakes.py8
-rw-r--r--nova/tests/api/openstack/test_accounts.py7
-rw-r--r--nova/tests/api/openstack/test_adminapi.py5
-rw-r--r--nova/tests/api/openstack/test_common.py60
-rw-r--r--nova/tests/api/openstack/test_extensions.py29
-rw-r--r--nova/tests/api/openstack/test_flavors_extra_specs.py3
-rw-r--r--nova/tests/api/openstack/test_images.py40
-rw-r--r--nova/tests/api/openstack/test_limits.py5
-rw-r--r--nova/tests/api/openstack/test_server_actions.py1075
-rw-r--r--nova/tests/api/openstack/test_server_metadata.py300
-rw-r--r--nova/tests/api/openstack/test_servers.py1076
-rw-r--r--nova/tests/api/openstack/test_users.py8
-rw-r--r--nova/tests/api/openstack/test_versions.py78
-rw-r--r--nova/tests/api/openstack/test_zones.py3
-rw-r--r--nova/tests/fake_utils.py8
-rw-r--r--nova/tests/hyperv_unittest.py5
-rw-r--r--nova/tests/image/test_glance.py36
-rw-r--r--nova/tests/image/test_s3.py10
-rw-r--r--nova/tests/integrated/integrated_helpers.py4
-rw-r--r--nova/tests/integrated/test_extensions.py5
-rw-r--r--nova/tests/integrated/test_login.py4
-rw-r--r--nova/tests/integrated/test_servers.py5
-rw-r--r--nova/tests/integrated/test_volumes.py5
-rw-r--r--nova/tests/integrated/test_xml.py5
-rw-r--r--nova/tests/scheduler/test_host_filter.py12
-rw-r--r--nova/tests/scheduler/test_least_cost_scheduler.py23
-rw-r--r--nova/tests/scheduler/test_scheduler.py43
-rw-r--r--nova/tests/scheduler/test_zone_aware_scheduler.py27
-rw-r--r--nova/tests/test_api.py36
-rw-r--r--nova/tests/test_auth.py11
-rw-r--r--nova/tests/test_block_device.py87
-rw-r--r--nova/tests/test_cloud.py161
-rw-r--r--nova/tests/test_compute.py507
-rw-r--r--nova/tests/test_host_filter.py12
-rw-r--r--nova/tests/test_hosts.py18
-rw-r--r--nova/tests/test_image.py134
-rw-r--r--nova/tests/test_instance_types_extra_specs.py2
-rw-r--r--nova/tests/test_ipv6.py3
-rw-r--r--nova/tests/test_libvirt.py75
-rw-r--r--nova/tests/test_metadata.py7
-rw-r--r--nova/tests/test_network.py2
-rw-r--r--nova/tests/test_nova_manage.py82
-rw-r--r--nova/tests/test_quota.py42
-rw-r--r--nova/tests/test_rpc.py2
-rw-r--r--nova/tests/test_rpc_amqp.py24
-rw-r--r--nova/tests/test_service.py1
-rw-r--r--nova/tests/test_skip_examples.py47
-rw-r--r--nova/tests/test_virt.py83
-rw-r--r--nova/tests/test_volume.py10
-rw-r--r--nova/tests/test_xenapi.py111
-rw-r--r--nova/tests/test_zones.py1
-rw-r--r--nova/utils.py33
-rw-r--r--nova/virt/disk.py47
-rw-r--r--nova/virt/driver.py35
-rw-r--r--nova/virt/fake.py10
-rw-r--r--nova/virt/hyperv.py10
-rw-r--r--nova/virt/libvirt.xml.template33
-rw-r--r--nova/virt/libvirt/connection.py230
-rw-r--r--nova/virt/libvirt/vif.py19
-rw-r--r--nova/virt/vmwareapi/io_util.py5
-rw-r--r--nova/virt/vmwareapi/vif.py2
-rw-r--r--nova/virt/vmwareapi/vim_util.py10
-rw-r--r--nova/virt/vmwareapi/vmware_images.py6
-rw-r--r--nova/virt/vmwareapi_conn.py6
-rw-r--r--nova/virt/xenapi/vm_utils.py19
-rw-r--r--nova/virt/xenapi/vmops.py157
-rw-r--r--nova/virt/xenapi/volume_utils.py4
-rw-r--r--nova/virt/xenapi_conn.py38
-rw-r--r--nova/vnc/proxy.py4
-rw-r--r--nova/volume/driver.py101
123 files changed, 6370 insertions, 2056 deletions
diff --git a/nova/api/direct.py b/nova/api/direct.py
index 993815fc7..fdd2943d2 100644
--- a/nova/api/direct.py
+++ b/nova/api/direct.py
@@ -296,8 +296,8 @@ class ServiceWrapper(object):
'application/json': nova.api.openstack.wsgi.JSONDictSerializer(),
}[content_type]
return serializer.serialize(result)
- except:
- raise exception.Error("returned non-serializable type: %s"
+ except Exception, e:
+ raise exception.Error(_("Returned non-serializable type: %s")
% result)
diff --git a/nova/api/ec2/__init__.py b/nova/api/ec2/__init__.py
index af232edda..8b6e47cfb 100644
--- a/nova/api/ec2/__init__.py
+++ b/nova/api/ec2/__init__.py
@@ -147,7 +147,7 @@ class Authenticate(wsgi.Middleware):
try:
signature = req.params['Signature']
access = req.params['AWSAccessKeyId']
- except:
+ except KeyError, e:
raise webob.exc.HTTPBadRequest()
# Make a copy of args for authentication and signature verification.
@@ -211,7 +211,7 @@ class Requestify(wsgi.Middleware):
for non_arg in non_args:
# Remove, but raise KeyError if omitted
args.pop(non_arg)
- except:
+ except KeyError, e:
raise webob.exc.HTTPBadRequest()
LOG.debug(_('action: %s'), action)
@@ -354,6 +354,10 @@ class Executor(wsgi.Application):
LOG.debug(_('KeyPairExists raised: %s'), unicode(ex),
context=context)
return self._error(req, context, type(ex).__name__, unicode(ex))
+ except exception.InvalidParameterValue as ex:
+ LOG.debug(_('InvalidParameterValue raised: %s'), unicode(ex),
+ context=context)
+ return self._error(req, context, type(ex).__name__, unicode(ex))
except Exception as ex:
extra = {'environment': req.environ}
LOG.exception(_('Unexpected error raised: %s'), unicode(ex),
diff --git a/nova/api/ec2/apirequest.py b/nova/api/ec2/apirequest.py
index 7d78c5cfa..9a3e55925 100644
--- a/nova/api/ec2/apirequest.py
+++ b/nova/api/ec2/apirequest.py
@@ -104,7 +104,7 @@ class APIRequest(object):
for key in data.keys():
val = data[key]
el.appendChild(self._render_data(xml, key, val))
- except:
+ except Exception:
LOG.debug(data)
raise
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 9ab8aeb58..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,8 +175,10 @@ 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()
+
mapper.resource("image_meta", "metadata",
controller=image_metadata_controller,
parent_resource=dict(member_name='image',
@@ -181,7 +189,14 @@ class APIRouterV11(APIRouter):
action='update_all',
conditions={"method": ['PUT']})
- mapper.resource("server_meta", "meta",
- controller=server_metadata.create_resource(),
+ server_metadata_controller = server_metadata.create_resource()
+
+ mapper.resource("server_meta", "metadata",
+ controller=server_metadata_controller,
parent_resource=dict(member_name='server',
collection_name='servers'))
+
+ mapper.connect("metadata", "/servers/{server_id}/metadata",
+ controller=server_metadata_controller,
+ action='update_all',
+ conditions={"method": ['PUT']})
diff --git a/nova/api/openstack/common.py b/nova/api/openstack/common.py
index efa4ab385..dfdd62201 100644
--- a/nova/api/openstack/common.py
+++ b/nova/api/openstack/common.py
@@ -15,8 +15,9 @@
# License for the specific language governing permissions and limitations
# under the License.
+import functools
import re
-from urlparse import urlparse
+import urlparse
from xml.dom import minidom
import webob
@@ -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.
@@ -137,8 +172,8 @@ def get_id_from_href(href):
if re.match(r'\d+$', str(href)):
return int(href)
try:
- return int(urlparse(href).path.split('/')[-1])
- except:
+ 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'))
@@ -153,22 +188,18 @@ def remove_version_from_href(href):
Returns: 'http://www.nova.com'
"""
- try:
- #removes the first instance that matches /v#.#/
- new_href = re.sub(r'[/][v][0-9]+\.[0-9]+[/]', '/', href, count=1)
+ parsed_url = urlparse.urlsplit(href)
+ new_path = re.sub(r'^/v[0-9]+\.[0-9]+(/|$)', r'\1', parsed_url.path,
+ count=1)
- #if no version was found, try finding /v#.# at the end of the string
- if new_href == href:
- new_href = re.sub(r'[/][v][0-9]+\.[0-9]+$', '', href, count=1)
- except:
- LOG.debug(_("Error removing version from href: %s") % href)
- msg = _('could not parse version from href')
+ if new_path == parsed_url.path:
+ msg = _('href %s does not contain version') % href
+ LOG.debug(msg)
raise ValueError(msg)
- if new_href == href:
- msg = _('href does not contain version')
- raise ValueError(msg)
- return new_href
+ parsed_url = list(parsed_url)
+ parsed_url[2] = new_path
+ return urlparse.urlunsplit(parsed_url)
def get_version_from_href(href):
@@ -196,7 +227,27 @@ def get_version_from_href(href):
return version
-class MetadataXMLDeserializer(wsgi.MetadataXMLDeserializer):
+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):
+ """Marshal the metadata attribute of a parsed request"""
+ if metadata_node is None:
+ return {}
+ metadata = {}
+ for meta_node in self.find_children_named(metadata_node, "meta"):
+ key = meta_node.getAttribute("key")
+ metadata[key] = self.extract_text(meta_node)
+ return metadata
def _extract_metadata_container(self, datastring):
dom = minidom.parseString(datastring)
@@ -247,7 +298,7 @@ class MetadataXMLSerializer(wsgi.XMLDictSerializer):
container_node = self.meta_list_to_xml(xml_doc, items)
xml_doc.appendChild(container_node)
self._add_xmlns(container_node)
- return xml_doc.toprettyxml(indent=' ', encoding='UTF-8')
+ return xml_doc.toxml('UTF-8')
def index(self, metadata_dict):
return self._meta_list_to_xml_string(metadata_dict)
@@ -264,7 +315,7 @@ class MetadataXMLSerializer(wsgi.XMLDictSerializer):
item_node = self._meta_item_to_xml(xml_doc, item_key, item_value)
xml_doc.appendChild(item_node)
self._add_xmlns(item_node)
- return xml_doc.toprettyxml(indent=' ', encoding='UTF-8')
+ return xml_doc.toxml('UTF-8')
def show(self, meta_item_dict):
return self._meta_item_to_xml_string(meta_item_dict['meta'])
@@ -274,3 +325,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..2aba1068a 100644
--- a/nova/api/openstack/contrib/floating_ips.py
+++ b/nova/api/openstack/contrib/floating_ips.py
@@ -18,12 +18,16 @@
from webob import exc
from nova import exception
+from nova import log as logging
from nova import network
from nova import rpc
from nova.api.openstack import faults
from nova.api.openstack import extensions
+LOG = logging.getLogger('nova.api.openstack.contrib.floating_ips')
+
+
def _translate_floating_ip_view(floating_ip):
result = {'id': floating_ip['id'],
'ip': floating_ip['address']}
@@ -97,8 +101,14 @@ class FloatingIPController(object):
def delete(self, req, id):
context = req.environ['nova.context']
-
ip = self.network_api.get_floating_ip(context, id)
+
+ if 'fixed_ip' in ip:
+ try:
+ self.disassociate(req, id, '')
+ except Exception as e:
+ LOG.exception(_("Error disassociating fixed_ip %s"), e)
+
self.network_api.release_floating_ip(context, address=ip)
return {'released': {
diff --git a/nova/api/openstack/contrib/hosts.py b/nova/api/openstack/contrib/hosts.py
index 55e57e1a4..ecaa365b7 100644
--- a/nova/api/openstack/contrib/hosts.py
+++ b/nova/api/openstack/contrib/hosts.py
@@ -24,6 +24,7 @@ from nova import log as logging
from nova.api.openstack import common
from nova.api.openstack import extensions
from nova.api.openstack import faults
+from nova.api.openstack.contrib import admin_only
from nova.scheduler import api as scheduler_api
@@ -70,7 +71,7 @@ class HostController(object):
key = raw_key.lower().strip()
val = raw_val.lower().strip()
# NOTE: (dabo) Right now only 'status' can be set, but other
- # actions may follow.
+ # settings may follow.
if key == "status":
if val[:6] in ("enable", "disabl"):
return self._set_enabled_status(req, id,
@@ -89,8 +90,30 @@ class HostController(object):
LOG.audit(_("Setting host %(host)s to %(state)s.") % locals())
result = self.compute_api.set_host_enabled(context, host=host,
enabled=enabled)
+ if result not in ("enabled", "disabled"):
+ # An error message was returned
+ raise webob.exc.HTTPBadRequest(explanation=result)
return {"host": host, "status": result}
+ def _host_power_action(self, req, host, action):
+ """Reboots, shuts down or powers up the host."""
+ context = req.environ['nova.context']
+ try:
+ result = self.compute_api.host_power_action(context, host=host,
+ action=action)
+ except NotImplementedError as e:
+ raise webob.exc.HTTPBadRequest(explanation=e.msg)
+ return {"host": host, "power_action": result}
+
+ def startup(self, req, id):
+ return self._host_power_action(req, host=id, action="startup")
+
+ def shutdown(self, req, id):
+ return self._host_power_action(req, host=id, action="shutdown")
+
+ def reboot(self, req, id):
+ return self._host_power_action(req, host=id, action="reboot")
+
class Hosts(extensions.ExtensionDescriptor):
def get_name(self):
@@ -108,7 +131,10 @@ class Hosts(extensions.ExtensionDescriptor):
def get_updated(self):
return "2011-06-29T00:00:00+00:00"
+ @admin_only.admin_only
def get_resources(self):
- resources = [extensions.ResourceExtension('os-hosts', HostController(),
- collection_actions={'update': 'PUT'}, member_actions={})]
+ resources = [extensions.ResourceExtension('os-hosts',
+ HostController(), collection_actions={'update': 'PUT'},
+ member_actions={"startup": "GET", "shutdown": "GET",
+ "reboot": "GET"})]
return resources
diff --git a/nova/api/openstack/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/create_instance_helper.py b/nova/api/openstack/create_instance_helper.py
index a2d18d37e..1425521a9 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
@@ -29,6 +27,7 @@ from nova import quota
from nova import utils
from nova.compute import instance_types
+from nova.api.openstack import common
from nova.api.openstack import wsgi
@@ -92,7 +91,7 @@ class CreateInstanceHelper(object):
image_href = self.controller._image_ref_from_req_data(body)
# If the image href was generated by nova api, strip image_href
# down to an id and use the default glance connection params
-
+
if str(image_href).startswith(req.application_url):
image_href = image_href.split('/').pop()
try:
@@ -293,7 +292,7 @@ class CreateInstanceHelper(object):
return password
-class ServerXMLDeserializer(wsgi.MetadataXMLDeserializer):
+class ServerXMLDeserializer(wsgi.XMLDeserializer):
"""
Deserializer to handle xml-formatted server create requests.
@@ -301,6 +300,56 @@ class ServerXMLDeserializer(wsgi.MetadataXMLDeserializer):
and personality attributes
"""
+ 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]
@@ -309,6 +358,12 @@ class ServerXMLDeserializer(wsgi.MetadataXMLDeserializer):
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)
@@ -322,6 +377,46 @@ class ServerXMLDeserializer(wsgi.MetadataXMLDeserializer):
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:
@@ -329,7 +424,10 @@ class ServerXMLDeserializer(wsgi.MetadataXMLDeserializer):
if value:
data[attribute] = value
metadata_node = self.find_first_child_named(node, 'metadata')
- data['metadata'] = self.extract_metadata(metadata_node)
+ if metadata_node is not None:
+ metadata = self.metadata_deserializer.extract_metadata(
+ metadata_node)
+ data['metadata'] = metadata
return data
def create(self, string):
@@ -343,28 +441,32 @@ class ServerXMLDeserializer(wsgi.MetadataXMLDeserializer):
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.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..b1538950f 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')
@@ -460,7 +461,11 @@ 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):
+ if not collection_actions:
+ collection_actions = {}
+ if not member_actions:
+ member_actions = {}
self.collection = collection
self.controller = controller
self.parent = parent
@@ -470,36 +475,38 @@ class ResourceExtension(object):
class ExtensionsXMLSerializer(wsgi.XMLDictSerializer):
+ NSMAP = {None: xmlutil.XMLNS_V11, 'atom': xmlutil.XMLNS_ATOM}
+
def show(self, ext_dict):
- ext = self._create_ext_elem(ext_dict['extension'])
+ ext = etree.Element('extension', nsmap=self.NSMAP)
+ self._populate_ext(ext, ext_dict['extension'])
return self._to_xml(ext)
def index(self, exts_dict):
- exts = ElementTree.Element('extensions')
+ exts = etree.Element('extensions', nsmap=self.NSMAP)
for ext_dict in exts_dict['extensions']:
- exts.append(self._create_ext_elem(ext_dict))
+ ext = etree.SubElement(exts, 'extension')
+ self._populate_ext(ext, ext_dict)
return self._to_xml(exts)
- def _create_ext_elem(self, ext_dict):
- """Create an extension xml element from a dict."""
- ext_elem = ElementTree.Element('extension')
+ def _populate_ext(self, ext_elem, ext_dict):
+ """Populate an extension xml element from a dict."""
+
ext_elem.set('name', ext_dict['name'])
ext_elem.set('namespace', ext_dict['namespace'])
ext_elem.set('alias', ext_dict['alias'])
ext_elem.set('updated', ext_dict['updated'])
- desc = ElementTree.Element('description')
+ desc = etree.Element('description')
desc.text = ext_dict['description']
ext_elem.append(desc)
for link in ext_dict.get('links', []):
- elem = ElementTree.Element('atom:link')
+ elem = etree.SubElement(ext_elem, '{%s}link' % xmlutil.XMLNS_ATOM)
elem.set('rel', link['rel'])
elem.set('href', link['href'])
elem.set('type', link['type'])
- ext_elem.append(elem)
return ext_elem
def _to_xml(self, root):
- """Convert the xml tree object to an xml string."""
- root.set('xmlns', wsgi.XMLNS_V11)
- root.set('xmlns:atom', wsgi.XMLNS_ATOM)
- return ElementTree.tostring(root, encoding='UTF-8')
+ """Convert the xml object to an xml string."""
+
+ return etree.tostring(root, encoding='UTF-8')
diff --git a/nova/api/openstack/image_metadata.py b/nova/api/openstack/image_metadata.py
index aaf64a123..4d615ea96 100644
--- a/nova/api/openstack/image_metadata.py
+++ b/nova/api/openstack/image_metadata.py
@@ -19,7 +19,6 @@ from webob import exc
from nova import flags
from nova import image
-from nova import quota
from nova import utils
from nova.api.openstack import common
from nova.api.openstack import wsgi
@@ -40,15 +39,6 @@ class Controller(object):
metadata = image.get('properties', {})
return metadata
- def _check_quota_limit(self, context, metadata):
- if metadata is None:
- return
- num_metadata = len(metadata)
- quota_metadata = quota.allowed_metadata_items(context, num_metadata)
- if quota_metadata < num_metadata:
- expl = _("Image metadata limit exceeded")
- raise exc.HTTPBadRequest(explanation=expl)
-
def index(self, req, image_id):
"""Returns the list of metadata for a given instance"""
context = req.environ['nova.context']
@@ -70,7 +60,7 @@ class Controller(object):
if 'metadata' in body:
for key, value in body['metadata'].iteritems():
metadata[key] = value
- self._check_quota_limit(context, metadata)
+ common.check_img_metadata_quota_limit(context, metadata)
img['properties'] = metadata
self.image_service.update(context, image_id, img, None)
return dict(metadata=metadata)
@@ -93,7 +83,7 @@ class Controller(object):
img = self.image_service.show(context, image_id)
metadata = self._get_metadata(context, image_id, img)
metadata[id] = meta[id]
- self._check_quota_limit(context, metadata)
+ common.check_img_metadata_quota_limit(context, metadata)
img['properties'] = metadata
self.image_service.update(context, image_id, img, None)
return dict(meta=meta)
@@ -102,7 +92,7 @@ class Controller(object):
context = req.environ['nova.context']
img = self.image_service.show(context, image_id)
metadata = body.get('metadata', {})
- self._check_quota_limit(context, metadata)
+ common.check_img_metadata_quota_limit(context, metadata)
img['properties'] = metadata
self.image_service.update(context, image_id, img, None)
return dict(metadata=metadata)
diff --git a/nova/api/openstack/images.py b/nova/api/openstack/images.py
index 0834adfa5..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:
@@ -116,13 +117,17 @@ class ControllerV10(Controller):
try:
image_name = image["name"]
- server_id = image["serverId"]
+ instance_id = image["serverId"]
except KeyError as missing_key:
msg = _("Image entity requires %s") % missing_key
raise webob.exc.HTTPBadRequest(explanation=msg)
context = req.environ["nova.context"]
- image = self._compute_service.snapshot(context, server_id, image_name)
+ props = {'instance_id': instance_id}
+ image = self._compute_service.snapshot(context,
+ instance_id,
+ image_name,
+ extra_properties=props)
return dict(image=self.get_builder(req).build(image, detail=True))
@@ -139,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])
@@ -152,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 d4f42bbf5..2b235f79a 100644
--- a/nova/api/openstack/server_metadata.py
+++ b/nova/api/openstack/server_metadata.py
@@ -18,6 +18,7 @@
from webob import exc
from nova import compute
+from nova.api.openstack import common
from nova.api.openstack import wsgi
from nova import exception
from nova import quota
@@ -31,89 +32,122 @@ class Controller(object):
super(Controller, self).__init__()
def _get_metadata(self, context, server_id):
- metadata = self.compute_api.get_instance_metadata(context, server_id)
+ try:
+ meta = self.compute_api.get_instance_metadata(context, server_id)
+ except exception.InstanceNotFound:
+ msg = _('Server does not exist')
+ raise exc.HTTPNotFound(explanation=msg)
+
meta_dict = {}
- for key, value in metadata.iteritems():
+ for key, value in meta.iteritems():
meta_dict[key] = value
- return dict(metadata=meta_dict)
-
- def _check_body(self, body):
- if body == None or body == "":
- expl = _('No Request Body')
- raise exc.HTTPBadRequest(explanation=expl)
+ return meta_dict
def index(self, req, server_id):
""" Returns the list of metadata for a given instance """
context = req.environ['nova.context']
- try:
- return self._get_metadata(context, server_id)
- except exception.InstanceNotFound:
- msg = _('Server %(server_id)s does not exist') % locals()
- raise exc.HTTPNotFound(explanation=msg)
+ return {'metadata': self._get_metadata(context, server_id)}
def create(self, req, server_id, body):
- self._check_body(body)
- context = req.environ['nova.context']
- metadata = body.get('metadata')
try:
- self.compute_api.update_or_create_instance_metadata(context,
- server_id,
- metadata)
- except exception.InstanceNotFound:
- msg = _('Server %(server_id)s does not exist') % locals()
- raise exc.HTTPNotFound(explanation=msg)
+ metadata = body['metadata']
+ except (KeyError, TypeError):
+ msg = _("Malformed request body")
+ raise exc.HTTPBadRequest(explanation=msg)
- except quota.QuotaError as error:
- self._handle_quota_error(error)
+ context = req.environ['nova.context']
- return body
+ new_metadata = self._update_instance_metadata(context,
+ server_id,
+ metadata,
+ delete=False)
+
+ return {'metadata': new_metadata}
def update(self, req, server_id, id, body):
- self._check_body(body)
- context = req.environ['nova.context']
- if not id in body:
+ try:
+ meta_item = body['meta']
+ except (TypeError, KeyError):
+ expl = _('Malformed request body')
+ raise exc.HTTPBadRequest(explanation=expl)
+
+ try:
+ meta_value = meta_item[id]
+ except (AttributeError, KeyError):
expl = _('Request body and URI mismatch')
raise exc.HTTPBadRequest(explanation=expl)
- if len(body) > 1:
+
+ if len(meta_item) > 1:
expl = _('Request body contains too many items')
raise exc.HTTPBadRequest(explanation=expl)
+
+ context = req.environ['nova.context']
+ self._update_instance_metadata(context,
+ server_id,
+ meta_item,
+ delete=False)
+
+ return {'meta': meta_item}
+
+ def update_all(self, req, server_id, body):
+ try:
+ metadata = body['metadata']
+ except (TypeError, KeyError):
+ expl = _('Malformed request body')
+ raise exc.HTTPBadRequest(explanation=expl)
+
+ context = req.environ['nova.context']
+ new_metadata = self._update_instance_metadata(context,
+ server_id,
+ metadata,
+ delete=True)
+
+ return {'metadata': new_metadata}
+
+ def _update_instance_metadata(self, context, server_id, metadata,
+ delete=False):
try:
- self.compute_api.update_or_create_instance_metadata(context,
- server_id,
- body)
+ return self.compute_api.update_instance_metadata(context,
+ server_id,
+ metadata,
+ delete)
+
except exception.InstanceNotFound:
- msg = _('Server %(server_id)s does not exist') % locals()
+ msg = _('Server does not exist')
raise exc.HTTPNotFound(explanation=msg)
+ except (ValueError, AttributeError):
+ msg = _("Malformed request body")
+ raise exc.HTTPBadRequest(explanation=msg)
+
except quota.QuotaError as error:
self._handle_quota_error(error)
- return body
-
def show(self, req, server_id, id):
""" Return a single metadata item """
context = req.environ['nova.context']
- try:
- data = self._get_metadata(context, server_id)
- except exception.InstanceNotFound:
- msg = _('Server %(server_id)s does not exist') % locals()
- raise exc.HTTPNotFound(explanation=msg)
+ data = self._get_metadata(context, server_id)
try:
- return {id: data['metadata'][id]}
+ return {'meta': {id: data[id]}}
except KeyError:
- msg = _("metadata item %s was not found" % (id))
+ msg = _("Metadata item was not found")
raise exc.HTTPNotFound(explanation=msg)
def delete(self, req, server_id, id):
""" Deletes an existing metadata """
context = req.environ['nova.context']
+
+ metadata = self._get_metadata(context, server_id)
+
try:
- self.compute_api.delete_instance_metadata(context, server_id, id)
- except exception.InstanceNotFound:
- msg = _('Server %(server_id)s does not exist') % locals()
+ 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, id)
+
def _handle_quota_error(self, error):
"""Reraise quota errors as api-specific http exceptions."""
if error.code == "MetadataLimitExceeded":
@@ -122,10 +156,16 @@ class Controller(object):
def create_resource():
- body_serializers = {
- 'application/xml': wsgi.XMLDictSerializer(xmlns=wsgi.XMLNS_V11),
+ headers_serializer = common.MetadataHeadersSerializer()
+
+ body_deserializers = {
+ 'application/xml': common.MetadataXMLDeserializer(),
}
- serializer = wsgi.ResponseSerializer(body_serializers)
+ body_serializers = {
+ 'application/xml': common.MetadataXMLSerializer(),
+ }
+ serializer = wsgi.ResponseSerializer(body_serializers, headers_serializer)
+ deserializer = wsgi.RequestDeserializer(body_deserializers)
- return wsgi.Resource(Controller(), serializer=serializer)
+ return wsgi.Resource(Controller(), deserializer, serializer)
diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py
index 002b47edb..736fdf6ce 100644
--- a/nova/api/openstack/servers.py
+++ b/nova/api/openstack/servers.py
@@ -44,7 +44,7 @@ FLAGS = flags.FLAGS
class Controller(object):
- """ The Server API controller for the OpenStack API """
+ """ The Server API base controller class for the OpenStack API """
def __init__(self):
self.compute_api = compute.API()
@@ -53,17 +53,21 @@ class Controller(object):
def index(self, req):
""" Returns a list of server names and ids for a given user """
try:
- servers = self._items(req, is_detail=False)
+ servers = self._get_servers(req, is_detail=False)
except exception.Invalid as err:
return exc.HTTPBadRequest(explanation=str(err))
+ except exception.NotFound:
+ return exc.HTTPNotFound()
return servers
def detail(self, req):
""" Returns a list of server details for a given user """
try:
- servers = self._items(req, is_detail=True)
+ servers = self._get_servers(req, is_detail=True)
except exception.Invalid as err:
return exc.HTTPBadRequest(explanation=str(err))
+ except exception.NotFound as err:
+ return exc.HTTPNotFound()
return servers
def _build_view(self, req, instance, is_detail=False):
@@ -75,22 +79,55 @@ class Controller(object):
def _action_rebuild(self, info, request, instance_id):
raise NotImplementedError()
- def _items(self, req, is_detail):
- """Returns a list of servers for a given user.
-
- builder - the response model builder
+ def _get_servers(self, req, is_detail):
+ """Returns a list of servers, taking into account any search
+ options specified.
"""
- query_str = req.str_GET
- reservation_id = query_str.get('reservation_id')
- project_id = query_str.get('project_id')
- fixed_ip = query_str.get('fixed_ip')
- recurse_zones = utils.bool_from_str(query_str.get('recurse_zones'))
+
+ search_opts = {}
+ search_opts.update(req.str_GET)
+
+ context = req.environ['nova.context']
+ remove_invalid_options(context, search_opts,
+ self._get_server_search_options())
+
+ # Convert recurse_zones into a boolean
+ search_opts['recurse_zones'] = utils.bool_from_str(
+ search_opts.get('recurse_zones', False))
+
+ # If search by 'status', we need to convert it to 'state'
+ # If the status is unknown, bail.
+ # Leave 'state' in search_opts so compute can pass it on to
+ # child zones..
+ if 'status' in search_opts:
+ status = search_opts['status']
+ search_opts['state'] = common.power_states_from_status(status)
+ if len(search_opts['state']) == 0:
+ reason = _('Invalid server status: %(status)s') % locals()
+ LOG.error(reason)
+ raise exception.InvalidInput(reason=reason)
+
+ # By default, compute's get_all() will return deleted instances.
+ # If an admin hasn't specified a 'deleted' search option, we need
+ # to filter out deleted instances by setting the filter ourselves.
+ # ... Unless 'changes-since' is specified, because 'changes-since'
+ # should return recently deleted images according to the API spec.
+
+ if 'deleted' not in search_opts:
+ # Admin hasn't specified deleted filter
+ if 'changes-since' not in search_opts:
+ # No 'changes-since', so we need to find non-deleted servers
+ search_opts['deleted'] = False
+ else:
+ # This is the default, but just in case..
+ search_opts['deleted'] = True
+
instance_list = self.compute_api.get_all(
- req.environ['nova.context'],
- reservation_id=reservation_id,
- project_id=project_id,
- fixed_ip=fixed_ip,
- recurse_zones=recurse_zones)
+ context, search_opts=search_opts)
+
+ # FIXME(comstud): 'changes-since' is not fully implemented. Where
+ # should this be filtered?
+
limited_list = self._limit_items(instance_list, req)
servers = [self._build_view(req, inst, is_detail)['server']
for inst in limited_list]
@@ -218,13 +255,14 @@ class Controller(object):
props = {'instance_ref': server_ref}
metadata = entity.get('metadata', {})
+ context = req.environ["nova.context"]
+ common.check_img_metadata_quota_limit(context, metadata)
try:
props.update(metadata)
except ValueError:
msg = _("Invalid metadata")
raise webob.exc.HTTPBadRequest(explanation=msg)
- context = req.environ["nova.context"]
image = self.compute_api.backup(context,
instance_id,
image_name,
@@ -240,6 +278,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 +306,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
@@ -290,7 +335,7 @@ class Controller(object):
context = req.environ['nova.context']
try:
self.compute_api.lock(context, id)
- except:
+ except Exception:
readable = traceback.format_exc()
LOG.exception(_("Compute.api::lock %s"), readable)
raise exc.HTTPUnprocessableEntity()
@@ -306,7 +351,7 @@ class Controller(object):
context = req.environ['nova.context']
try:
self.compute_api.unlock(context, id)
- except:
+ except Exception:
readable = traceback.format_exc()
LOG.exception(_("Compute.api::unlock %s"), readable)
raise exc.HTTPUnprocessableEntity()
@@ -321,14 +366,14 @@ class Controller(object):
context = req.environ['nova.context']
try:
self.compute_api.get_lock(context, id)
- except:
+ except Exception:
readable = traceback.format_exc()
LOG.exception(_("Compute.api::get_lock %s"), readable)
raise exc.HTTPUnprocessableEntity()
return webob.Response(status_int=202)
@scheduler_api.redirect_handler
- def reset_network(self, req, id, body):
+ def reset_network(self, req, id):
"""
Reset networking on an instance (admin only).
@@ -336,14 +381,14 @@ class Controller(object):
context = req.environ['nova.context']
try:
self.compute_api.reset_network(context, id)
- except:
+ except Exception:
readable = traceback.format_exc()
LOG.exception(_("Compute.api::reset_network %s"), readable)
raise exc.HTTPUnprocessableEntity()
return webob.Response(status_int=202)
@scheduler_api.redirect_handler
- def inject_network_info(self, req, id, body):
+ def inject_network_info(self, req, id):
"""
Inject network info for an instance (admin only).
@@ -351,55 +396,55 @@ class Controller(object):
context = req.environ['nova.context']
try:
self.compute_api.inject_network_info(context, id)
- except:
+ except Exception:
readable = traceback.format_exc()
LOG.exception(_("Compute.api::inject_network_info %s"), readable)
raise exc.HTTPUnprocessableEntity()
return webob.Response(status_int=202)
@scheduler_api.redirect_handler
- def pause(self, req, id, body):
+ def pause(self, req, id):
""" Permit Admins to Pause the server. """
ctxt = req.environ['nova.context']
try:
self.compute_api.pause(ctxt, id)
- except:
+ except Exception:
readable = traceback.format_exc()
LOG.exception(_("Compute.api::pause %s"), readable)
raise exc.HTTPUnprocessableEntity()
return webob.Response(status_int=202)
@scheduler_api.redirect_handler
- def unpause(self, req, id, body):
+ def unpause(self, req, id):
""" Permit Admins to Unpause the server. """
ctxt = req.environ['nova.context']
try:
self.compute_api.unpause(ctxt, id)
- except:
+ except Exception:
readable = traceback.format_exc()
LOG.exception(_("Compute.api::unpause %s"), readable)
raise exc.HTTPUnprocessableEntity()
return webob.Response(status_int=202)
@scheduler_api.redirect_handler
- def suspend(self, req, id, body):
+ def suspend(self, req, id):
"""permit admins to suspend the server"""
context = req.environ['nova.context']
try:
self.compute_api.suspend(context, id)
- except:
+ except Exception:
readable = traceback.format_exc()
LOG.exception(_("compute.api::suspend %s"), readable)
raise exc.HTTPUnprocessableEntity()
return webob.Response(status_int=202)
@scheduler_api.redirect_handler
- def resume(self, req, id, body):
+ def resume(self, req, id):
"""permit admins to resume the server from suspend"""
context = req.environ['nova.context']
try:
self.compute_api.resume(context, id)
- except:
+ except Exception:
readable = traceback.format_exc()
LOG.exception(_("compute.api::resume %s"), readable)
raise exc.HTTPUnprocessableEntity()
@@ -420,7 +465,7 @@ class Controller(object):
context = req.environ["nova.context"]
try:
self.compute_api.rescue(context, id)
- except:
+ except Exception:
readable = traceback.format_exc()
LOG.exception(_("compute.api::rescue %s"), readable)
raise exc.HTTPUnprocessableEntity()
@@ -432,7 +477,7 @@ class Controller(object):
context = req.environ["nova.context"]
try:
self.compute_api.unrescue(context, id)
- except:
+ except Exception:
readable = traceback.format_exc()
LOG.exception(_("compute.api::unrescue %s"), readable)
raise exc.HTTPUnprocessableEntity()
@@ -498,6 +543,7 @@ class Controller(object):
class ControllerV10(Controller):
+ """v1.0 OpenStack API controller"""
@scheduler_api.redirect_handler
def delete(self, req, id):
@@ -560,8 +606,13 @@ class ControllerV10(Controller):
""" Determine the admin password for a server on creation """
return self.helper._get_server_admin_password_old_style(server)
+ def _get_server_search_options(self):
+ """Return server search options allowed by non-admin"""
+ return 'reservation_id', 'fixed_ip', 'name', 'recurse_zones'
+
class ControllerV11(Controller):
+ """v1.1 OpenStack API controller"""
@scheduler_api.redirect_handler
def delete(self, req, id):
@@ -646,6 +697,9 @@ class ControllerV11(Controller):
""" 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 +734,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 +757,14 @@ class ControllerV11(Controller):
props = {'instance_ref': server_ref}
metadata = entity.get('metadata', {})
+ context = req.environ['nova.context']
+ common.check_img_metadata_quota_limit(context, metadata)
try:
props.update(metadata)
except ValueError:
msg = _("Invalid metadata")
raise webob.exc.HTTPBadRequest(explanation=msg)
- context = req.environ['nova.context']
image = self.compute_api.snapshot(context,
instance_id,
image_name,
@@ -729,9 +785,17 @@ class ControllerV11(Controller):
""" Determine the admin password for a server on creation """
return self.helper._get_server_admin_password_new_style(server)
+ def _get_server_search_options(self):
+ """Return server search options allowed by non-admin"""
+ return ('reservation_id', 'name', 'recurse_zones',
+ 'status', 'image', 'flavor', 'changes-since')
+
class HeadersSerializer(wsgi.ResponseHeadersSerializer):
+ def create(self, response, data):
+ response.status_int = 202
+
def delete(self, response, data):
response.status_int = 204
@@ -891,11 +955,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/versions.py b/nova/api/openstack/versions.py
index 3ef72b7f6..e2f892fb6 100644
--- a/nova/api/openstack/versions.py
+++ b/nova/api/openstack/versions.py
@@ -34,23 +34,23 @@ VERSIONS = {
"rel": "describedby",
"type": "application/pdf",
"href": "http://docs.rackspacecloud.com/"
- "servers/api/v1.0/cs-devguide-20110125.pdf"
+ "servers/api/v1.0/cs-devguide-20110125.pdf",
},
{
"rel": "describedby",
"type": "application/vnd.sun.wadl+xml",
"href": "http://docs.rackspacecloud.com/"
- "servers/api/v1.0/application.wadl"
+ "servers/api/v1.0/application.wadl",
},
],
"media-types": [
{
"base": "application/xml",
- "type": "application/vnd.openstack.compute-v1.0+xml"
+ "type": "application/vnd.openstack.compute-v1.0+xml",
},
{
"base": "application/json",
- "type": "application/vnd.openstack.compute-v1.0+json"
+ "type": "application/vnd.openstack.compute-v1.0+json",
}
],
},
@@ -63,23 +63,23 @@ VERSIONS = {
"rel": "describedby",
"type": "application/pdf",
"href": "http://docs.rackspacecloud.com/"
- "servers/api/v1.1/cs-devguide-20110125.pdf"
+ "servers/api/v1.1/cs-devguide-20110125.pdf",
},
{
"rel": "describedby",
"type": "application/vnd.sun.wadl+xml",
"href": "http://docs.rackspacecloud.com/"
- "servers/api/v1.1/application.wadl"
+ "servers/api/v1.1/application.wadl",
},
],
"media-types": [
{
"base": "application/xml",
- "type": "application/vnd.openstack.compute-v1.1+xml"
+ "type": "application/vnd.openstack.compute-v1.1+xml",
},
{
"base": "application/json",
- "type": "application/vnd.openstack.compute-v1.1+json"
+ "type": "application/vnd.openstack.compute-v1.1+json",
}
],
},
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/views/versions.py b/nova/api/openstack/views/versions.py
index 547289034..03da80818 100644
--- a/nova/api/openstack/views/versions.py
+++ b/nova/api/openstack/views/versions.py
@@ -42,10 +42,10 @@ class ViewBuilder(object):
"links": [
{
"rel": "self",
- "href": self.generate_href(version['id'], req.path)
- }
+ "href": self.generate_href(version['id'], req.path),
+ },
],
- "media-types": version['media-types']
+ "media-types": version['media-types'],
})
return dict(choices=version_objs)
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):
diff --git a/nova/auth/novarc.template b/nova/auth/novarc.template
index d05c099d7..978ffb210 100644
--- a/nova/auth/novarc.template
+++ b/nova/auth/novarc.template
@@ -16,3 +16,4 @@ export NOVA_API_KEY="%(access)s"
export NOVA_USERNAME="%(user)s"
export NOVA_PROJECT_ID="%(project)s"
export NOVA_URL="%(os)s"
+export NOVA_VERSION="1.1"
diff --git a/nova/block_device.py b/nova/block_device.py
new file mode 100644
index 000000000..8d95e0029
--- /dev/null
+++ b/nova/block_device.py
@@ -0,0 +1,71 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2011 Isaku Yamahata <yamahata@valinux co jp>
+# 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 re
+
+
+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
+
+
+_ephemeral = re.compile('^ephemeral(\d|[1-9]\d+)$')
+
+
+def is_ephemeral(device_name):
+ return _ephemeral.match(device_name)
+
+
+def ephemeral_num(ephemeral_name):
+ assert is_ephemeral(ephemeral_name)
+ return int(_ephemeral.sub('\\1', ephemeral_name))
+
+
+def is_swap_or_ephemeral(device_name):
+ return device_name == 'swap' or is_ephemeral(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 (is_swap_or_ephemeral(virtual) and
+ (not m['device'].startswith('/'))):
+ m['device'] = '/dev/' + m['device']
+ return mappings
+
+
+_dev = re.compile('^/dev/')
+
+
+def strip_dev(device_name):
+ """remove leading '/dev/'"""
+ return _dev.sub('', device_name)
diff --git a/nova/cloudpipe/pipelib.py b/nova/cloudpipe/pipelib.py
index 521525205..2c4673f9e 100644
--- a/nova/cloudpipe/pipelib.py
+++ b/nova/cloudpipe/pipelib.py
@@ -141,15 +141,12 @@ class CloudPipe(object):
try:
result = cloud._gen_key(context, context.user_id, key_name)
private_key = result['private_key']
- try:
- key_dir = os.path.join(FLAGS.keys_path, context.user_id)
- if not os.path.exists(key_dir):
- os.makedirs(key_dir)
- key_path = os.path.join(key_dir, '%s.pem' % key_name)
- with open(key_path, 'w') as f:
- f.write(private_key)
- except:
- pass
- except exception.Duplicate:
+ key_dir = os.path.join(FLAGS.keys_path, context.user_id)
+ if not os.path.exists(key_dir):
+ os.makedirs(key_dir)
+ key_path = os.path.join(key_dir, '%s.pem' % key_name)
+ with open(key_path, 'w') as f:
+ f.write(private_key)
+ except (exception.Duplicate, os.error, IOError):
pass
return key_name
diff --git a/nova/compute/api.py b/nova/compute/api.py
index aae16d1da..91a0c93b2 100644
--- a/nova/compute/api.py
+++ b/nova/compute/api.py
@@ -19,9 +19,11 @@
"""Handles all requests relating to instances (guest vms)."""
import eventlet
+import novaclient
import re
import time
+from nova import block_device
from nova import db
from nova import exception
from nova import flags
@@ -32,7 +34,6 @@ from nova import quota
from nova import rpc
from nova import utils
from nova import volume
-from nova.api.ec2 import ec2utils
from nova.compute import instance_types
from nova.compute import power_state
from nova.compute.utils import terminate_volumes
@@ -121,8 +122,10 @@ class API(base.Base):
if len(content) > content_limit:
raise quota.QuotaError(code="OnsetFileContentLimitExceeded")
- def _check_metadata_properties_quota(self, context, metadata={}):
+ def _check_metadata_properties_quota(self, context, metadata=None):
"""Enforce quota limits on metadata properties."""
+ if not metadata:
+ metadata = {}
num_metadata = len(metadata)
quota_metadata = quota.allowed_metadata_items(context, num_metadata)
if quota_metadata < num_metadata:
@@ -148,7 +151,7 @@ class API(base.Base):
min_count=None, max_count=None,
display_name='', display_description='',
key_name=None, key_data=None, security_group='default',
- availability_zone=None, user_data=None, metadata={},
+ availability_zone=None, user_data=None, metadata=None,
injected_files=None, admin_password=None, zone_blob=None,
reservation_id=None):
"""Verify all the input parameters regardless of the provisioning
@@ -160,6 +163,8 @@ class API(base.Base):
min_count = 1
if not max_count:
max_count = min_count
+ if not metadata:
+ metadata = {}
num_instances = quota.allowed_instances(context, max_count,
instance_type)
@@ -218,7 +223,7 @@ class API(base.Base):
if reservation_id is None:
reservation_id = utils.generate_uid('r')
- root_device_name = ec2utils.properties_root_device_name(
+ root_device_name = block_device.properties_root_device_name(
image['properties'])
base_options = {
@@ -250,34 +255,64 @@ class API(base.Base):
return (num_instances, base_options, image)
- def _update_image_block_device_mapping(self, elevated_context, instance_id,
+ @staticmethod
+ def _ephemeral_size(instance_type, ephemeral_name):
+ num = block_device.ephemeral_num(ephemeral_name)
+
+ # TODO(yamahata): ephemeralN where N > 0
+ # Only ephemeral0 is allowed for now because InstanceTypes
+ # table only allows single local disk, local_gb.
+ # In order to enhance it, we need to add a new columns to
+ # instance_types table.
+ if num > 0:
+ return 0
+
+ return instance_type.get('local_gb')
+
+ def _update_image_block_device_mapping(self, elevated_context,
+ instance_type, instance_id,
mappings):
"""tell vm driver to create ephemeral/swap device at boot time by
updating BlockDeviceMapping
"""
- for bdm in ec2utils.mappings_prepend_dev(mappings):
+ instance_type = (instance_type or
+ instance_types.get_default_instance_type())
+
+ for bdm in block_device.mappings_prepend_dev(mappings):
LOG.debug(_("bdm %s"), bdm)
virtual_name = bdm['virtual']
if virtual_name == 'ami' or virtual_name == 'root':
continue
- assert (virtual_name == 'swap' or
- virtual_name.startswith('ephemeral'))
+ if not block_device.is_swap_or_ephemeral(virtual_name):
+ continue
+
+ size = 0
+ if virtual_name == 'swap':
+ size = instance_type.get('swap', 0)
+ elif block_device.is_ephemeral(virtual_name):
+ size = self._ephemeral_size(instance_type, virtual_name)
+
+ if size == 0:
+ continue
+
values = {
'instance_id': instance_id,
'device_name': bdm['device'],
- 'virtual_name': virtual_name, }
+ 'virtual_name': virtual_name,
+ 'volume_size': size}
self.db.block_device_mapping_update_or_create(elevated_context,
values)
- def _update_block_device_mapping(self, elevated_context, instance_id,
+ def _update_block_device_mapping(self, elevated_context,
+ instance_type, instance_id,
block_device_mapping):
"""tell vm driver to attach volume at boot time by updating
BlockDeviceMapping
"""
+ LOG.debug(_("block_device_mapping %s"), block_device_mapping)
for bdm in block_device_mapping:
- LOG.debug(_('bdm %s'), bdm)
assert 'device_name' in bdm
values = {'instance_id': instance_id}
@@ -286,10 +321,18 @@ class API(base.Base):
'no_device'):
values[key] = bdm.get(key)
+ virtual_name = bdm.get('virtual_name')
+ if (virtual_name is not None and
+ block_device.is_ephemeral(virtual_name)):
+ size = self._ephemeral_size(instance_type, virtual_name)
+ if size == 0:
+ continue
+ values['volume_size'] = size
+
# NOTE(yamahata): NoDevice eliminates devices defined in image
# files by command line option.
# (--block-device-mapping)
- if bdm.get('virtual_name') == 'NoDevice':
+ if virtual_name == 'NoDevice':
values['no_device'] = True
for k in ('delete_on_termination', 'volume_id',
'snapshot_id', 'volume_id', 'volume_size',
@@ -299,8 +342,8 @@ class API(base.Base):
self.db.block_device_mapping_update_or_create(elevated_context,
values)
- def create_db_entry_for_new_instance(self, context, image, base_options,
- security_group, block_device_mapping, num=1):
+ def create_db_entry_for_new_instance(self, context, instance_type, image,
+ base_options, security_group, block_device_mapping, num=1):
"""Create an entry in the DB for this new instance,
including any related table updates (such as security group,
etc).
@@ -333,12 +376,12 @@ class API(base.Base):
security_group_id)
# BlockDeviceMapping table
- self._update_image_block_device_mapping(elevated, instance_id,
- image['properties'].get('mappings', []))
- self._update_block_device_mapping(elevated, instance_id,
+ self._update_image_block_device_mapping(elevated, instance_type,
+ instance_id, image['properties'].get('mappings', []))
+ self._update_block_device_mapping(elevated, instance_type, instance_id,
image['properties'].get('block_device_mapping', []))
# override via command line option
- self._update_block_device_mapping(elevated, instance_id,
+ self._update_block_device_mapping(elevated, instance_type, instance_id,
block_device_mapping)
# Set sane defaults if not specified
@@ -360,6 +403,7 @@ class API(base.Base):
instance_type, zone_blob,
availability_zone, injected_files,
admin_password,
+ image,
instance_id=None, num_instances=1):
"""Send the run_instance request to the schedulers for processing."""
pid = context.project_id
@@ -373,6 +417,7 @@ class API(base.Base):
filter_class = 'nova.scheduler.host_filter.InstanceTypeFilter'
request_spec = {
+ 'image': image,
'instance_properties': base_options,
'instance_type': instance_type,
'filter': filter_class,
@@ -395,12 +440,16 @@ class API(base.Base):
min_count=None, max_count=None,
display_name='', display_description='',
key_name=None, key_data=None, security_group='default',
- availability_zone=None, user_data=None, metadata={},
+ availability_zone=None, user_data=None, metadata=None,
injected_files=None, admin_password=None, zone_blob=None,
reservation_id=None, block_device_mapping=None):
"""Provision the instances by passing the whole request to
the Scheduler for execution. Returns a Reservation ID
related to the creation of all of these instances."""
+
+ if not metadata:
+ metadata = {}
+
num_instances, base_options, image = self._check_create_parameters(
context, instance_type,
image_href, kernel_id, ramdisk_id,
@@ -415,6 +464,7 @@ class API(base.Base):
instance_type, zone_blob,
availability_zone, injected_files,
admin_password,
+ image,
num_instances=num_instances)
return base_options['reservation_id']
@@ -424,7 +474,7 @@ class API(base.Base):
min_count=None, max_count=None,
display_name='', display_description='',
key_name=None, key_data=None, security_group='default',
- availability_zone=None, user_data=None, metadata={},
+ availability_zone=None, user_data=None, metadata=None,
injected_files=None, admin_password=None, zone_blob=None,
reservation_id=None, block_device_mapping=None):
"""
@@ -439,6 +489,9 @@ class API(base.Base):
Returns a list of instance dicts.
"""
+ if not metadata:
+ metadata = {}
+
num_instances, base_options, image = self._check_create_parameters(
context, instance_type,
image_href, kernel_id, ramdisk_id,
@@ -453,7 +506,8 @@ class API(base.Base):
instances = []
LOG.debug(_("Going to run %s instances..."), num_instances)
for num in range(num_instances):
- instance = self.create_db_entry_for_new_instance(context, image,
+ instance = self.create_db_entry_for_new_instance(context,
+ instance_type, image,
base_options, security_group,
block_device_mapping, num=num)
instances.append(instance)
@@ -463,6 +517,7 @@ class API(base.Base):
instance_type, zone_blob,
availability_zone, injected_files,
admin_password,
+ image,
instance_id=instance_id)
return [dict(x.iteritems()) for x in instances]
@@ -669,59 +724,84 @@ class API(base.Base):
"""
return self.get(context, instance_id)
- def get_all(self, context, project_id=None, reservation_id=None,
- fixed_ip=None, recurse_zones=False):
+ def get_all(self, context, search_opts=None):
"""Get all instances filtered by one of the given parameters.
If there is no filter and the context is an admin, it will retreive
all instances in the system.
"""
- if reservation_id is not None:
- recurse_zones = True
- instances = self.db.instance_get_all_by_reservation(
- context, reservation_id)
- elif fixed_ip is not None:
+ if search_opts is None:
+ search_opts = {}
+
+ LOG.debug(_("Searching by: %s") % str(search_opts))
+
+ # Fixups for the DB call
+ filters = {}
+
+ def _remap_flavor_filter(flavor_id):
+ instance_type = self.db.instance_type_get_by_flavor_id(
+ context, flavor_id)
+ filters['instance_type_id'] = instance_type['id']
+
+ def _remap_fixed_ip_filter(fixed_ip):
+ # Turn fixed_ip into a regexp match. Since '.' matches
+ # any character, we need to use regexp escaping for it.
+ filters['ip'] = '^%s$' % fixed_ip.replace('.', '\\.')
+
+ # search_option to filter_name mapping.
+ filter_mapping = {
+ 'image': 'image_ref',
+ 'name': 'display_name',
+ 'instance_name': 'name',
+ 'recurse_zones': None,
+ 'flavor': _remap_flavor_filter,
+ 'fixed_ip': _remap_fixed_ip_filter}
+
+ # copy from search_opts, doing various remappings as necessary
+ for opt, value in search_opts.iteritems():
+ # Do remappings.
+ # Values not in the filter_mapping table are copied as-is.
+ # If remapping is None, option is not copied
+ # If the remapping is a string, it is the filter_name to use
try:
- instances = self.db.fixed_ip_get_instance(context, fixed_ip)
- except exception.FloatingIpNotFound, e:
- if not recurse_zones:
- raise
- instances = None
- elif project_id or not context.is_admin:
- if not context.project_id:
- instances = self.db.instance_get_all_by_user(
- context, context.user_id)
+ remap_object = filter_mapping[opt]
+ except KeyError:
+ filters[opt] = value
else:
- if project_id is None:
- project_id = context.project_id
- instances = self.db.instance_get_all_by_project(
- context, project_id)
- else:
- instances = self.db.instance_get_all(context)
+ if remap_object:
+ if isinstance(remap_object, basestring):
+ filters[remap_object] = value
+ else:
+ remap_object(value)
+
+ recurse_zones = search_opts.get('recurse_zones', False)
+ if 'reservation_id' in filters:
+ recurse_zones = True
- if instances is None:
- instances = []
- elif not isinstance(instances, list):
- instances = [instances]
+ instances = self.db.instance_get_all_by_filters(context, filters)
if not recurse_zones:
return instances
+ # Recurse zones. Need admin context for this. Send along
+ # the un-modified search options we received..
admin_context = context.elevated()
children = scheduler_api.call_zone_method(admin_context,
"list",
+ errors_to_ignore=[novaclient.exceptions.NotFound],
novaclient_collection_name="servers",
- reservation_id=reservation_id,
- project_id=project_id,
- fixed_ip=fixed_ip,
- recurse_zones=True)
+ search_opts=search_opts)
for zone, servers in children:
+ # 'servers' can be None if a 404 was returned by a zone
+ if servers is None:
+ continue
for server in servers:
# Results are ready to send to user. No need to scrub.
server._info['_is_precooked'] = True
instances.append(server._info)
+
return instances
def _cast_compute_message(self, method, context, instance_id, host=None,
@@ -888,7 +968,7 @@ class API(base.Base):
params = {'migration_id': migration_ref['id']}
self._cast_compute_message('revert_resize', context,
instance_ref['uuid'],
- migration_ref['source_compute'],
+ migration_ref['dest_compute'],
params=params)
self.db.migration_update(context, migration_ref['id'],
@@ -908,7 +988,7 @@ class API(base.Base):
params = {'migration_id': migration_ref['id']}
self._cast_compute_message('confirm_resize', context,
instance_ref['uuid'],
- migration_ref['dest_compute'],
+ migration_ref['source_compute'],
params=params)
self.db.migration_update(context, migration_ref['id'],
@@ -955,7 +1035,7 @@ class API(base.Base):
{"method": "prep_resize",
"args": {"topic": FLAGS.compute_topic,
"instance_id": instance_ref['uuid'],
- "flavor_id": new_instance_type['id']}})
+ "instance_type_id": new_instance_type['id']}})
@scheduler_api.reroute_compute("add_fixed_ip")
def add_fixed_ip(self, context, instance_id, network_id):
@@ -993,7 +1073,12 @@ class API(base.Base):
def set_host_enabled(self, context, host, enabled):
"""Sets the specified host's ability to accept new instances."""
return self._call_compute_message("set_host_enabled", context,
- instance_id=None, host=host, params={"enabled": enabled})
+ host=host, params={"enabled": enabled})
+
+ def host_power_action(self, context, host, action):
+ """Reboots, shuts down or powers up the host."""
+ return self._call_compute_message("host_power_action", context,
+ host=host, params={"action": action})
@scheduler_api.reroute_compute("diagnostics")
def get_diagnostics(self, context, instance_id):
@@ -1166,11 +1251,20 @@ class API(base.Base):
"""Delete the given metadata item from an instance."""
self.db.instance_metadata_delete(context, instance_id, key)
- def update_or_create_instance_metadata(self, context, instance_id,
- metadata):
- """Updates or creates instance metadata."""
- combined_metadata = self.get_instance_metadata(context, instance_id)
- combined_metadata.update(metadata)
- self._check_metadata_properties_quota(context, combined_metadata)
- self.db.instance_metadata_update_or_create(context, instance_id,
- metadata)
+ def update_instance_metadata(self, context, instance_id,
+ metadata, delete=False):
+ """Updates or creates instance metadata.
+
+ If delete is True, metadata items that are not specified in the
+ `metadata` argument will be deleted.
+
+ """
+ if delete:
+ _metadata = metadata
+ else:
+ _metadata = self.get_instance_metadata(context, instance_id)
+ _metadata.update(metadata)
+
+ self._check_metadata_properties_quota(context, _metadata)
+ self.db.instance_metadata_update(context, instance_id, _metadata, True)
+ return _metadata
diff --git a/nova/compute/manager.py b/nova/compute/manager.py
index cf4ee229d..d38213083 100644
--- a/nova/compute/manager.py
+++ b/nova/compute/manager.py
@@ -44,6 +44,8 @@ import functools
from eventlet import greenthread
+import nova.context
+from nova import block_device
from nova import exception
from nova import flags
import nova.image
@@ -147,6 +149,31 @@ class ComputeManager(manager.SchedulerDependentManager):
def init_host(self):
"""Initialization for a standalone compute service."""
self.driver.init_host(host=self.host)
+ context = nova.context.get_admin_context()
+ instances = self.db.instance_get_all_by_host(context, self.host)
+ for instance in instances:
+ inst_name = instance['name']
+ db_state = instance['state']
+ drv_state = self._update_state(context, instance['id'])
+
+ expect_running = db_state == power_state.RUNNING \
+ and drv_state != db_state
+
+ LOG.debug(_('Current state of %(inst_name)s is %(drv_state)s, '
+ 'state in DB is %(db_state)s.'), locals())
+
+ if (expect_running and FLAGS.resume_guests_state_on_host_boot)\
+ or FLAGS.start_guests_on_host_boot:
+ LOG.info(_('Rebooting instance %(inst_name)s after '
+ 'nova-compute restart.'), locals())
+ self.reboot_instance(context, instance['id'])
+ elif drv_state == power_state.RUNNING:
+ # Hyper-V and VMWareAPI drivers will raise and exception
+ try:
+ self.driver.ensure_filtering_rules_for_instance(instance)
+ except NotImplementedError:
+ LOG.warning(_('Hypervisor driver does not '
+ 'support firewall rules'))
def _update_state(self, context, instance_id, state=None):
"""Update the state of an instance from the driver info."""
@@ -154,6 +181,7 @@ class ComputeManager(manager.SchedulerDependentManager):
if state is None:
try:
+ LOG.debug(_('Checking state of %s'), instance_ref['name'])
info = self.driver.get_info(instance_ref['name'])
except exception.NotFound:
info = None
@@ -164,6 +192,7 @@ class ComputeManager(manager.SchedulerDependentManager):
state = power_state.FAILED
self.db.instance_set_state(context, instance_id, state)
+ return state
def _update_launched_at(self, context, instance_id, launched_at=None):
"""Update the launched_at parameter of the given instance."""
@@ -232,6 +261,8 @@ class ComputeManager(manager.SchedulerDependentManager):
volume_api = volume.API()
block_device_mapping = []
+ swap = None
+ ephemerals = []
for bdm in self.db.block_device_mapping_get_all_by_instance(
context, instance_id):
LOG.debug(_("setting up bdm %s"), bdm)
@@ -239,11 +270,18 @@ class ComputeManager(manager.SchedulerDependentManager):
if bdm['no_device']:
continue
if bdm['virtual_name']:
- # TODO(yamahata):
- # block devices for swap and ephemeralN will be
- # created by virt driver locally in compute node.
- assert (bdm['virtual_name'] == 'swap' or
- bdm['virtual_name'].startswith('ephemeral'))
+ virtual_name = bdm['virtual_name']
+ device_name = bdm['device_name']
+ assert block_device.is_swap_or_ephemeral(virtual_name)
+ if virtual_name == 'swap':
+ swap = {'device_name': device_name,
+ 'swap_size': bdm['volume_size']}
+ elif block_device.is_ephemeral(virtual_name):
+ eph = {'num': block_device.ephemeral_num(virtual_name),
+ 'virtual_name': virtual_name,
+ 'device_name': device_name,
+ 'size': bdm['volume_size']}
+ ephemerals.append(eph)
continue
if ((bdm['snapshot_id'] is not None) and
@@ -279,7 +317,7 @@ class ComputeManager(manager.SchedulerDependentManager):
'mount_device':
bdm['device_name']})
- return block_device_mapping
+ return (swap, ephemerals, block_device_mapping)
def _run_instance(self, context, instance_id, **kwargs):
"""Launch a new instance with specified options."""
@@ -320,13 +358,21 @@ class ComputeManager(manager.SchedulerDependentManager):
# all vif creation and network injection, maybe this is correct
network_info = []
- bd_mapping = self._setup_block_device_mapping(context, instance_id)
+ (swap, ephemerals,
+ block_device_mapping) = self._setup_block_device_mapping(
+ context, instance_id)
+ block_device_info = {
+ 'root_device_name': instance['root_device_name'],
+ 'swap': swap,
+ 'ephemerals': ephemerals,
+ 'block_device_mapping': block_device_mapping}
# TODO(vish) check to make sure the availability zone matches
self._update_state(context, instance_id, power_state.BUILDING)
try:
- self.driver.spawn(context, instance, network_info, bd_mapping)
+ self.driver.spawn(context, instance,
+ network_info, block_device_info)
except Exception as ex: # pylint: disable=W0702
msg = _("Instance '%(instance_id)s' failed to spawn. Is "
"virtualization enabled in the BIOS? Details: "
@@ -720,7 +766,8 @@ class ComputeManager(manager.SchedulerDependentManager):
instance_ref['host'])
rpc.cast(context, topic,
{'method': 'finish_revert_resize',
- 'args': {'migration_id': migration_ref['id']},
+ 'args': {'instance_id': instance_ref['uuid'],
+ 'migration_id': migration_ref['id']},
})
@exception.wrap_exception(notifier=notifier, publisher_id=publisher_id())
@@ -736,8 +783,8 @@ class ComputeManager(manager.SchedulerDependentManager):
instance_ref = self.db.instance_get_by_uuid(context,
migration_ref.instance_uuid)
- instance_type = self.db.instance_type_get_by_flavor_id(context,
- migration_ref['old_flavor_id'])
+ instance_type = self.db.instance_type_get(context,
+ migration_ref['old_instance_type_id'])
# Just roll back the record. There's no need to resize down since
# the 'old' VM already has the preferred attributes
@@ -758,7 +805,7 @@ class ComputeManager(manager.SchedulerDependentManager):
@exception.wrap_exception(notifier=notifier, publisher_id=publisher_id())
@checks_instance_lock
- def prep_resize(self, context, instance_id, flavor_id):
+ def prep_resize(self, context, instance_id, instance_type_id):
"""Initiates the process of moving a running instance to another host.
Possibly changes the RAM and disk size in the process.
@@ -777,16 +824,16 @@ class ComputeManager(manager.SchedulerDependentManager):
old_instance_type = self.db.instance_type_get(context,
instance_ref['instance_type_id'])
- new_instance_type = self.db.instance_type_get_by_flavor_id(context,
- flavor_id)
+ new_instance_type = self.db.instance_type_get(context,
+ instance_type_id)
migration_ref = self.db.migration_create(context,
{'instance_uuid': instance_ref['uuid'],
'source_compute': instance_ref['host'],
'dest_compute': FLAGS.host,
'dest_host': self.driver.get_host_ip_addr(),
- 'old_flavor_id': old_instance_type['flavorid'],
- 'new_flavor_id': flavor_id,
+ 'old_instance_type_id': old_instance_type['id'],
+ 'new_instance_type_id': instance_type_id,
'status': 'pre-migrating'})
LOG.audit(_('instance %s: migrating'), instance_ref['uuid'],
@@ -849,9 +896,10 @@ class ComputeManager(manager.SchedulerDependentManager):
resize_instance = False
instance_ref = self.db.instance_get_by_uuid(context,
migration_ref.instance_uuid)
- if migration_ref['old_flavor_id'] != migration_ref['new_flavor_id']:
- instance_type = self.db.instance_type_get_by_flavor_id(context,
- migration_ref['new_flavor_id'])
+ if migration_ref['old_instance_type_id'] != \
+ migration_ref['new_instance_type_id']:
+ instance_type = self.db.instance_type_get(context,
+ migration_ref['new_instance_type_id'])
self.db.instance_update(context, instance_ref.uuid,
dict(instance_type_id=instance_type['id'],
memory_mb=instance_type['memory_mb'],
@@ -928,8 +976,12 @@ class ComputeManager(manager.SchedulerDependentManager):
result))
@exception.wrap_exception(notifier=notifier, publisher_id=publisher_id())
- def set_host_enabled(self, context, instance_id=None, host=None,
- enabled=None):
+ def host_power_action(self, context, host=None, action=None):
+ """Reboots, shuts down or powers up the host."""
+ return self.driver.host_power_action(host, action)
+
+ @exception.wrap_exception(notifier=notifier, publisher_id=publisher_id())
+ def set_host_enabled(self, context, host=None, enabled=None):
"""Sets the specified host's ability to accept new instances."""
return self.driver.set_host_enabled(host, enabled)
diff --git a/nova/console/xvp.py b/nova/console/xvp.py
index 3cd287183..2d6842044 100644
--- a/nova/console/xvp.py
+++ b/nova/console/xvp.py
@@ -20,7 +20,6 @@
import fcntl
import os
import signal
-import subprocess
from Cheetah import Template
diff --git a/nova/crypto.py b/nova/crypto.py
index 8d535f426..71bef80f2 100644
--- a/nova/crypto.py
+++ b/nova/crypto.py
@@ -104,6 +104,12 @@ def fetch_ca(project_id=None, chain=True):
return buffer
+def generate_fingerprint(public_key):
+ (out, err) = utils.execute('ssh-keygen', '-q', '-l', '-f', public_key)
+ fingerprint = out.split(' ')[1]
+ return fingerprint
+
+
def generate_key_pair(bits=1024):
# what is the magic 65537?
@@ -111,9 +117,7 @@ def generate_key_pair(bits=1024):
keyfile = os.path.join(tmpdir, 'temp')
utils.execute('ssh-keygen', '-q', '-b', bits, '-N', '',
'-f', keyfile)
- (out, err) = utils.execute('ssh-keygen', '-q', '-l', '-f',
- '%s.pub' % (keyfile))
- fingerprint = out.split(' ')[1]
+ fingerprint = generate_fingerprint('%s.pub' % (keyfile))
private_key = open(keyfile).read()
public_key = open(keyfile + '.pub').read()
diff --git a/nova/db/api.py b/nova/db/api.py
index 47308bdba..3d0727f8b 100644
--- a/nova/db/api.py
+++ b/nova/db/api.py
@@ -387,15 +387,6 @@ def fixed_ip_get_by_virtual_interface(context, vif_id):
return IMPL.fixed_ip_get_by_virtual_interface(context, vif_id)
-def fixed_ip_get_instance(context, address):
- """Get an instance for a fixed ip by address."""
- return IMPL.fixed_ip_get_instance(context, address)
-
-
-def fixed_ip_get_instance_v6(context, address):
- return IMPL.fixed_ip_get_instance_v6(context, address)
-
-
def fixed_ip_get_network(context, address):
"""Get a network for a fixed ip by address."""
return IMPL.fixed_ip_get_network(context, address)
@@ -500,6 +491,11 @@ def instance_get_all(context):
return IMPL.instance_get_all(context)
+def instance_get_all_by_filters(context, filters):
+ """Get all instances that match all filters."""
+ return IMPL.instance_get_all_by_filters(context, filters)
+
+
def instance_get_active_by_window(context, begin, end=None):
"""Get instances active during a certain time window."""
return IMPL.instance_get_active_by_window(context, begin, end)
@@ -521,10 +517,20 @@ def instance_get_all_by_host(context, host):
def instance_get_all_by_reservation(context, reservation_id):
- """Get all instance belonging to a reservation."""
+ """Get all instances belonging to a reservation."""
return IMPL.instance_get_all_by_reservation(context, reservation_id)
+def instance_get_by_fixed_ip(context, address):
+ """Get an instance for a fixed ip by address."""
+ return IMPL.instance_get_by_fixed_ip(context, address)
+
+
+def instance_get_by_fixed_ipv6(context, address):
+ """Get an instance for a fixed ip by IPv6 address."""
+ return IMPL.instance_get_by_fixed_ipv6(context, address)
+
+
def instance_get_fixed_addresses(context, instance_id):
"""Get the fixed ip address of an instance."""
return IMPL.instance_get_fixed_addresses(context, instance_id)
@@ -1381,9 +1387,9 @@ def instance_metadata_delete(context, instance_id, key):
IMPL.instance_metadata_delete(context, instance_id, key)
-def instance_metadata_update_or_create(context, instance_id, metadata):
- """Create or update instance metadata."""
- IMPL.instance_metadata_update_or_create(context, instance_id, metadata)
+def instance_metadata_update(context, instance_id, metadata, delete):
+ """Update metadata if it exists, otherwise create it."""
+ IMPL.instance_metadata_update(context, instance_id, metadata, delete)
####################
diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py
index 4f1445217..8119cdfb8 100644
--- a/nova/db/sqlalchemy/api.py
+++ b/nova/db/sqlalchemy/api.py
@@ -18,8 +18,10 @@
"""
Implementation of SQLAlchemy backend.
"""
+import re
import warnings
+from nova import block_device
from nova import db
from nova import exception
from nova import flags
@@ -828,28 +830,6 @@ def fixed_ip_get_by_virtual_interface(context, vif_id):
return rv
-@require_context
-def fixed_ip_get_instance(context, address):
- fixed_ip_ref = fixed_ip_get_by_address(context, address)
- return fixed_ip_ref.instance
-
-
-@require_context
-def fixed_ip_get_instance_v6(context, address):
- session = get_session()
-
- # convert IPv6 address to mac
- mac = ipv6.to_mac(address)
-
- # get virtual interface
- vif_ref = virtual_interface_get_by_address(context, mac)
-
- # look up instance based on instance_id from vif row
- result = session.query(models.Instance).\
- filter_by(id=vif_ref['instance_id'])
- return result
-
-
@require_admin_context
def fixed_ip_get_network(context, address):
fixed_ip_ref = fixed_ip_get_by_address(context, address)
@@ -1168,6 +1148,114 @@ def instance_get_all(context):
all()
+@require_context
+def instance_get_all_by_filters(context, filters):
+ """Return instances that match all filters. Deleted instances
+ will be returned by default, unless there's a filter that says
+ otherwise"""
+
+ def _regexp_filter_by_ipv6(instance, filter_re):
+ for interface in instance['virtual_interfaces']:
+ fixed_ipv6 = interface.get('fixed_ipv6')
+ if fixed_ipv6 and filter_re.match(fixed_ipv6):
+ return True
+ return False
+
+ def _regexp_filter_by_ip(instance, filter_re):
+ for interface in instance['virtual_interfaces']:
+ for fixed_ip in interface['fixed_ips']:
+ if not fixed_ip or not fixed_ip['address']:
+ continue
+ if filter_re.match(fixed_ip['address']):
+ return True
+ for floating_ip in fixed_ip.get('floating_ips', []):
+ if not floating_ip or not floating_ip['address']:
+ continue
+ if filter_re.match(floating_ip['address']):
+ return True
+ return False
+
+ def _regexp_filter_by_column(instance, filter_name, filter_re):
+ try:
+ v = getattr(instance, filter_name)
+ except AttributeError:
+ return True
+ if v and filter_re.match(str(v)):
+ return True
+ return False
+
+ def _exact_match_filter(query, column, value):
+ """Do exact match against a column. value to match can be a list
+ so you can match any value in the list.
+ """
+ if isinstance(value, list):
+ column_attr = getattr(models.Instance, column)
+ return query.filter(column_attr.in_(value))
+ else:
+ filter_dict = {}
+ filter_dict[column] = value
+ return query.filter_by(**filter_dict)
+
+ session = get_session()
+ query_prefix = session.query(models.Instance).\
+ options(joinedload_all('fixed_ips.floating_ips')).\
+ options(joinedload_all('virtual_interfaces.network')).\
+ options(joinedload_all(
+ 'virtual_interfaces.fixed_ips.floating_ips')).\
+ options(joinedload('security_groups')).\
+ options(joinedload_all('fixed_ips.network')).\
+ options(joinedload('metadata')).\
+ options(joinedload('instance_type'))
+
+ # Make a copy of the filters dictionary to use going forward, as we'll
+ # be modifying it and we shouldn't affect the caller's use of it.
+ filters = filters.copy()
+
+ if not context.is_admin:
+ # If we're not admin context, add appropriate filter..
+ if context.project_id:
+ filters['project_id'] = context.project_id
+ else:
+ filters['user_id'] = context.user_id
+
+ # Filters for exact matches that we can do along with the SQL query...
+ # For other filters that don't match this, we will do regexp matching
+ exact_match_filter_names = ['project_id', 'user_id', 'image_ref',
+ 'state', 'instance_type_id', 'deleted']
+
+ query_filters = [key for key in filters.iterkeys()
+ if key in exact_match_filter_names]
+
+ for filter_name in query_filters:
+ # Do the matching and remove the filter from the dictionary
+ # so we don't try it again below..
+ query_prefix = _exact_match_filter(query_prefix, filter_name,
+ filters.pop(filter_name))
+
+ instances = query_prefix.all()
+
+ if not instances:
+ return []
+
+ # Now filter on everything else for regexp matching..
+ # For filters not in the list, we'll attempt to use the filter_name
+ # as a column name in Instance..
+ regexp_filter_funcs = {'ip6': _regexp_filter_by_ipv6,
+ 'ip': _regexp_filter_by_ip}
+
+ for filter_name in filters.iterkeys():
+ filter_func = regexp_filter_funcs.get(filter_name, None)
+ filter_re = re.compile(str(filters[filter_name]))
+ if filter_func:
+ filter_l = lambda instance: filter_func(instance, filter_re)
+ else:
+ filter_l = lambda instance: _regexp_filter_by_column(instance,
+ filter_name, filter_re)
+ instances = filter(filter_l, instances)
+
+ return instances
+
+
@require_admin_context
def instance_get_active_by_window(context, begin, end=None):
"""Return instances that were continuously active over the given window"""
@@ -1236,30 +1324,48 @@ def instance_get_all_by_project(context, project_id):
@require_context
def instance_get_all_by_reservation(context, reservation_id):
session = get_session()
+ query = session.query(models.Instance).\
+ filter_by(reservation_id=reservation_id).\
+ options(joinedload_all('fixed_ips.floating_ips')).\
+ options(joinedload('virtual_interfaces')).\
+ options(joinedload('security_groups')).\
+ options(joinedload_all('fixed_ips.network')).\
+ options(joinedload('metadata')).\
+ options(joinedload('instance_type'))
if is_admin_context(context):
- return session.query(models.Instance).\
- options(joinedload_all('fixed_ips.floating_ips')).\
- options(joinedload('virtual_interfaces')).\
- options(joinedload('security_groups')).\
- options(joinedload_all('fixed_ips.network')).\
- options(joinedload('metadata')).\
- options(joinedload('instance_type')).\
- filter_by(reservation_id=reservation_id).\
- filter_by(deleted=can_read_deleted(context)).\
- all()
+ return query.\
+ filter_by(deleted=can_read_deleted(context)).\
+ all()
elif is_user_context(context):
- return session.query(models.Instance).\
- options(joinedload_all('fixed_ips.floating_ips')).\
- options(joinedload('virtual_interfaces')).\
- options(joinedload('security_groups')).\
- options(joinedload_all('fixed_ips.network')).\
- options(joinedload('metadata')).\
- options(joinedload('instance_type')).\
- filter_by(project_id=context.project_id).\
- filter_by(reservation_id=reservation_id).\
- filter_by(deleted=False).\
- all()
+ return query.\
+ filter_by(project_id=context.project_id).\
+ filter_by(deleted=False).\
+ all()
+
+
+@require_context
+def instance_get_by_fixed_ip(context, address):
+ """Return instance ref by exact match of FixedIP"""
+ fixed_ip_ref = fixed_ip_get_by_address(context, address)
+ return fixed_ip_ref.instance
+
+
+@require_context
+def instance_get_by_fixed_ipv6(context, address):
+ """Return instance ref by exact match of IPv6"""
+ session = get_session()
+
+ # convert IPv6 address to mac
+ mac = ipv6.to_mac(address)
+
+ # get virtual interface
+ vif_ref = virtual_interface_get_by_address(context, mac)
+
+ # look up instance based on instance_id from vif row
+ result = session.query(models.Instance).\
+ filter_by(id=vif_ref['instance_id'])
+ return result
@require_admin_context
@@ -1301,7 +1407,7 @@ def instance_get_fixed_addresses_v6(context, instance_id):
network_refs = network_get_all_by_instance(context, instance_id)
# compile a list of cidr_v6 prefixes sorted by network id
prefixes = [ref.cidr_v6 for ref in
- sorted(network_refs, key=lambda ref: ref.id)]
+ sorted(network_refs, key=lambda ref: ref.id)]
# get vifs associated with instance
vif_refs = virtual_interface_get_by_instance(context, instance_ref.id)
# compile list of the mac_addresses for vifs sorted by network id
@@ -1345,9 +1451,10 @@ def instance_update(context, instance_id, values):
session = get_session()
metadata = values.get('metadata')
if metadata is not None:
- instance_metadata_delete_all(context, instance_id)
- instance_metadata_update_or_create(context, instance_id,
- values.pop('metadata'))
+ instance_metadata_update(context,
+ instance_id,
+ values.pop('metadata'),
+ delete=True)
with session.begin():
if utils.is_uuid_like(instance_id):
instance_ref = instance_get_by_uuid(context, instance_id,
@@ -1426,9 +1533,14 @@ def instance_action_create(context, values):
def instance_get_actions(context, instance_id):
"""Return the actions associated to the given instance id"""
session = get_session()
+
+ if utils.is_uuid_like(instance_id):
+ instance = instance_get_by_uuid(context, instance_id, session)
+ instance_id = instance.id
+
return session.query(models.InstanceActions).\
filter_by(instance_id=instance_id).\
- all()
+ all()
###################
@@ -1681,7 +1793,9 @@ def network_get_by_cidr(context, cidr):
session = get_session()
result = session.query(models.Network).\
filter(or_(models.Network.cidr == cidr,
- models.Network.cidr_v6 == cidr)).first()
+ models.Network.cidr_v6 == cidr)).\
+ filter_by(deleted=False).\
+ first()
if not result:
raise exception.NetworkNotFoundForCidr(cidr=cidr)
@@ -2265,6 +2379,20 @@ def block_device_mapping_update_or_create(context, values):
else:
result.update(values)
+ # NOTE(yamahata): same virtual device name can be specified multiple
+ # times. So delete the existing ones.
+ virtual_name = values['virtual_name']
+ if (virtual_name is not None and
+ block_device.is_swap_or_ephemeral(virtual_name)):
+ session.query(models.BlockDeviceMapping).\
+ filter_by(instance_id=values['instance_id']).\
+ filter_by(virtual_name=virtual_name).\
+ filter(models.BlockDeviceMapping.device_name !=
+ values['device_name']).\
+ update({'deleted': True,
+ 'deleted_at': utils.utcnow(),
+ 'updated_at': literal_column('updated_at')})
+
@require_context
def block_device_mapping_get_all_by_instance(context, instance_id):
@@ -3178,8 +3306,9 @@ def instance_metadata_delete_all(context, instance_id):
@require_context
@require_instance_exists
-def instance_metadata_get_item(context, instance_id, key):
- session = get_session()
+def instance_metadata_get_item(context, instance_id, key, session=None):
+ if not session:
+ session = get_session()
meta_result = session.query(models.InstanceMetadata).\
filter_by(instance_id=instance_id).\
@@ -3195,21 +3324,35 @@ def instance_metadata_get_item(context, instance_id, key):
@require_context
@require_instance_exists
-def instance_metadata_update_or_create(context, instance_id, metadata):
+def instance_metadata_update(context, instance_id, metadata, delete):
session = get_session()
- original_metadata = instance_metadata_get(context, instance_id)
+ # Set existing metadata to deleted if delete argument is True
+ if delete:
+ original_metadata = instance_metadata_get(context, instance_id)
+ for meta_key, meta_value in original_metadata.iteritems():
+ if meta_key not in metadata:
+ meta_ref = instance_metadata_get_item(context, instance_id,
+ meta_key, session)
+ meta_ref.update({'deleted': True})
+ meta_ref.save(session=session)
meta_ref = None
- for key, value in metadata.iteritems():
+
+ # Now update all existing items with new values, or create new meta objects
+ for meta_key, meta_value in metadata.iteritems():
+
+ # update the value whether it exists or not
+ item = {"value": meta_value}
+
try:
- meta_ref = instance_metadata_get_item(context, instance_id, key,
- session)
- except:
+ meta_ref = instance_metadata_get_item(context, instance_id,
+ meta_key, session)
+ except exception.InstanceMetadataNotFound, e:
meta_ref = models.InstanceMetadata()
- meta_ref.update({"key": key, "value": value,
- "instance_id": instance_id,
- "deleted": False})
+ item.update({"key": meta_key, "instance_id": instance_id})
+
+ meta_ref.update(item)
meta_ref.save(session=session)
return metadata
@@ -3300,8 +3443,11 @@ def instance_type_extra_specs_delete(context, instance_type_id, key):
@require_context
-def instance_type_extra_specs_get_item(context, instance_type_id, key):
- session = get_session()
+def instance_type_extra_specs_get_item(context, instance_type_id, key,
+ session=None):
+
+ if not session:
+ session = get_session()
spec_result = session.query(models.InstanceTypeExtraSpecs).\
filter_by(instance_type_id=instance_type_id).\
@@ -3323,11 +3469,9 @@ def instance_type_extra_specs_update_or_create(context, instance_type_id,
spec_ref = None
for key, value in specs.iteritems():
try:
- spec_ref = instance_type_extra_specs_get_item(context,
- instance_type_id,
- key,
- session)
- except:
+ spec_ref = instance_type_extra_specs_get_item(
+ context, instance_type_id, key, session)
+ except exception.InstanceTypeExtraSpecsNotFound, e:
spec_ref = models.InstanceTypeExtraSpecs()
spec_ref.update({"key": key, "value": value,
"instance_type_id": instance_type_id,
diff --git a/nova/db/sqlalchemy/migrate_repo/versions/036_change_flavor_id_in_migrations.py b/nova/db/sqlalchemy/migrate_repo/versions/036_change_flavor_id_in_migrations.py
new file mode 100644
index 000000000..f3244033b
--- /dev/null
+++ b/nova/db/sqlalchemy/migrate_repo/versions/036_change_flavor_id_in_migrations.py
@@ -0,0 +1,72 @@
+# 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.from sqlalchemy import *
+
+from sqlalchemy import Column, Integer, MetaData, Table
+
+
+meta = MetaData()
+
+
+#
+# Tables to alter
+#
+#
+
+old_flavor_id = Column('old_flavor_id', Integer())
+new_flavor_id = Column('new_flavor_id', Integer())
+old_instance_type_id = Column('old_instance_type_id', Integer())
+new_instance_type_id = Column('new_instance_type_id', Integer())
+
+
+def upgrade(migrate_engine):
+ meta.bind = migrate_engine
+ instance_types = Table('instance_types', meta, autoload=True)
+ migrations = Table('migrations', meta, autoload=True)
+ migrations.create_column(old_instance_type_id)
+ migrations.create_column(new_instance_type_id)
+
+ # Convert flavor_id to instance_type_id
+ for instance_type in migrate_engine.execute(instance_types.select()):
+ migrate_engine.execute(migrations.update()\
+ .where(migrations.c.old_flavor_id == instance_type.flavorid)\
+ .values(old_instance_type_id=instance_type.id))
+ migrate_engine.execute(migrations.update()\
+ .where(migrations.c.new_flavor_id == instance_type.flavorid)\
+ .values(new_instance_type_id=instance_type.id))
+
+ migrations.c.old_flavor_id.drop()
+ migrations.c.new_flavor_id.drop()
+
+
+def downgrade(migrate_engine):
+ meta.bind = migrate_engine
+ instance_types = Table('instance_types', meta, autoload=True)
+ migrations = Table('migrations', meta, autoload=True)
+ migrations.create_column(old_flavor_id)
+ migrations.create_column(new_flavor_id)
+
+ # Convert instance_type_id to flavor_id
+ for instance_type in migrate_engine.execute(instance_types.select()):
+ migrate_engine.execute(migrations.update()\
+ .where(migrations.c.old_instance_type_id == instance_type.id)\
+ .values(old_flavor_id=instance_type.flavorid))
+ migrate_engine.execute(migrations.update()\
+ .where(migrations.c.new_instance_type_id == instance_type.id)\
+ .values(new_flavor_id=instance_type.flavorid))
+
+ migrations.c.old_instance_type_id.drop()
+ migrations.c.new_instance_type_id.drop()
diff --git a/nova/db/sqlalchemy/models.py b/nova/db/sqlalchemy/models.py
index 056259539..939fde199 100644
--- a/nova/db/sqlalchemy/models.py
+++ b/nova/db/sqlalchemy/models.py
@@ -180,6 +180,7 @@ class Instance(BASE, NovaBase):
image_ref = Column(String(255))
kernel_id = Column(String(255))
ramdisk_id = Column(String(255))
+ server_name = Column(String(255))
# image_ref = Column(Integer, ForeignKey('images.id'), nullable=True)
# kernel_id = Column(Integer, ForeignKey('images.id'), nullable=True)
@@ -202,7 +203,7 @@ class Instance(BASE, NovaBase):
hostname = Column(String(255))
host = Column(String(255)) # , ForeignKey('hosts.id'))
- # aka flavor_id
+ # *not* flavor_id
instance_type_id = Column(Integer)
user_data = Column(Text)
@@ -511,8 +512,8 @@ class Migration(BASE, NovaBase):
source_compute = Column(String(255))
dest_compute = Column(String(255))
dest_host = Column(String(255))
- old_flavor_id = Column(Integer())
- new_flavor_id = Column(Integer())
+ old_instance_type_id = Column(Integer())
+ new_instance_type_id = Column(Integer())
instance_uuid = Column(String(255), ForeignKey('instances.uuid'),
nullable=True)
#TODO(_cerberus_): enum
diff --git a/nova/exception.py b/nova/exception.py
index 68e6ac937..0d60cb0bf 100644
--- a/nova/exception.py
+++ b/nova/exception.py
@@ -25,6 +25,7 @@ SHOULD include dedicated exception logging.
"""
from functools import wraps
+import sys
from nova import log as logging
@@ -96,6 +97,10 @@ def wrap_exception(notifier=None, publisher_id=None, event_type=None,
try:
return f(*args, **kw)
except Exception, e:
+ # Save exception since it can be clobbered during processing
+ # below before we can re-raise
+ exc_info = sys.exc_info()
+
if notifier:
payload = dict(args=args, exception=e)
payload.update(kw)
@@ -122,7 +127,9 @@ def wrap_exception(notifier=None, publisher_id=None, event_type=None,
LOG.exception(_('Uncaught exception'))
#logging.error(traceback.extract_stack(exc_traceback))
raise Error(str(e))
- raise
+
+ # re-raise original exception since it may have been clobbered
+ raise exc_info[0], exc_info[1], exc_info[2]
return wraps(f)(wrapped)
return inner
@@ -150,6 +157,10 @@ class NovaException(Exception):
return self._error_string
+class ImagePaginationFailed(NovaException):
+ message = _("Failed to paginate through images from image service")
+
+
class VirtualInterfaceCreateException(NovaException):
message = _("Virtual Interface creation failed")
@@ -198,6 +209,12 @@ class InvalidContentType(Invalid):
message = _("Invalid content type %(content_type)s.")
+# Cannot be templated as the error syntax varies.
+# msg needs to be constructed when raised.
+class InvalidParameterValue(Invalid):
+ message = _("%(err)s")
+
+
class InstanceNotRunning(Invalid):
message = _("Instance %(instance_id)s is not running.")
diff --git a/nova/flags.py b/nova/flags.py
index fa6d8860a..7916501a4 100644
--- a/nova/flags.py
+++ b/nova/flags.py
@@ -317,7 +317,7 @@ DEFINE_string('osapi_extensions_path', '/var/lib/nova/extensions',
DEFINE_string('osapi_host', '$my_ip', 'ip of api server')
DEFINE_string('osapi_scheme', 'http', 'prefix for openstack')
DEFINE_integer('osapi_port', 8774, 'OpenStack API port')
-DEFINE_string('osapi_path', '/v1.0/', 'suffix for openstack')
+DEFINE_string('osapi_path', '/v1.1/', 'suffix for openstack')
DEFINE_integer('osapi_max_limit', 1000,
'max number of items returned in a collection response')
@@ -387,3 +387,11 @@ DEFINE_list('zone_capabilities',
'Key/Multi-value list representng capabilities of this zone')
DEFINE_string('build_plan_encryption_key', None,
'128bit (hex) encryption key for scheduler build plans.')
+
+DEFINE_bool('start_guests_on_host_boot', False,
+ 'Whether to restart guests when the host reboots')
+DEFINE_bool('resume_guests_state_on_host_boot', False,
+ 'Whether to start guests, that was running before the host reboot')
+
+DEFINE_string('root_helper', 'sudo',
+ 'Command prefix to use for running commands as root')
diff --git a/nova/image/__init__.py b/nova/image/__init__.py
index a27d649d4..5447c8a3a 100644
--- a/nova/image/__init__.py
+++ b/nova/image/__init__.py
@@ -35,6 +35,7 @@ def _parse_image_ref(image_href):
:param image_href: href of an image
:returns: a tuple of the form (image_id, host, port)
+ :raises ValueError
"""
o = urlparse(image_href)
@@ -72,7 +73,7 @@ def get_glance_client(image_href):
try:
(image_id, host, port) = _parse_image_ref(image_href)
- except:
+ except ValueError:
raise exception.InvalidImageRef(image_href=image_href)
glance_client = GlanceClient(host, port)
return (glance_client, image_id)
diff --git a/nova/image/fake.py b/nova/image/fake.py
index 28e912534..97af81711 100644
--- a/nova/image/fake.py
+++ b/nova/image/fake.py
@@ -45,9 +45,12 @@ class _FakeImageService(service.BaseImageService):
'name': 'fakeimage123456',
'created_at': timestamp,
'updated_at': timestamp,
+ 'deleted_at': None,
+ 'deleted': False,
'status': 'active',
- 'container_format': 'ami',
- 'disk_format': 'raw',
+ 'is_public': False,
+# 'container_format': 'ami',
+# 'disk_format': 'raw',
'properties': {'kernel_id': FLAGS.null_kernel,
'ramdisk_id': FLAGS.null_kernel,
'architecture': 'x86_64'}}
@@ -56,9 +59,12 @@ class _FakeImageService(service.BaseImageService):
'name': 'fakeimage123456',
'created_at': timestamp,
'updated_at': timestamp,
+ 'deleted_at': None,
+ 'deleted': False,
'status': 'active',
- 'container_format': 'ami',
- 'disk_format': 'raw',
+ 'is_public': True,
+# 'container_format': 'ami',
+# 'disk_format': 'raw',
'properties': {'kernel_id': FLAGS.null_kernel,
'ramdisk_id': FLAGS.null_kernel}}
@@ -66,9 +72,12 @@ class _FakeImageService(service.BaseImageService):
'name': 'fakeimage123456',
'created_at': timestamp,
'updated_at': timestamp,
+ 'deleted_at': None,
+ 'deleted': False,
'status': 'active',
- 'container_format': 'ami',
- 'disk_format': 'raw',
+ 'is_public': True,
+# 'container_format': 'ami',
+# 'disk_format': 'raw',
'properties': {'kernel_id': FLAGS.null_kernel,
'ramdisk_id': FLAGS.null_kernel}}
@@ -76,9 +85,12 @@ class _FakeImageService(service.BaseImageService):
'name': 'fakeimage123456',
'created_at': timestamp,
'updated_at': timestamp,
+ 'deleted_at': None,
+ 'deleted': False,
'status': 'active',
- 'container_format': 'ami',
- 'disk_format': 'raw',
+ 'is_public': True,
+# 'container_format': 'ami',
+# 'disk_format': 'raw',
'properties': {'kernel_id': FLAGS.null_kernel,
'ramdisk_id': FLAGS.null_kernel}}
@@ -86,9 +98,12 @@ class _FakeImageService(service.BaseImageService):
'name': 'fakeimage123456',
'created_at': timestamp,
'updated_at': timestamp,
+ 'deleted_at': None,
+ 'deleted': False,
'status': 'active',
- 'container_format': 'ami',
- 'disk_format': 'raw',
+ 'is_public': True,
+# 'container_format': 'ami',
+# 'disk_format': 'raw',
'properties': {'kernel_id': FLAGS.null_kernel,
'ramdisk_id': FLAGS.null_kernel}}
@@ -101,7 +116,11 @@ class _FakeImageService(service.BaseImageService):
def index(self, context, filters=None, marker=None, limit=None):
"""Returns list of images."""
- return copy.deepcopy(self.images.values())
+ retval = []
+ for img in self.images.values():
+ retval += [dict([(k, v) for k, v in img.iteritems()
+ if k in ['id', 'name']])]
+ return retval
def detail(self, context, filters=None, marker=None, limit=None):
"""Return list of detailed image information."""
diff --git a/nova/image/glance.py b/nova/image/glance.py
index 44a3c6f83..9060f6a91 100644
--- a/nova/image/glance.py
+++ b/nova/image/glance.py
@@ -19,7 +19,9 @@
from __future__ import absolute_import
+import copy
import datetime
+import json
import random
from glance.common import exception as glance_exception
@@ -87,42 +89,71 @@ class GlanceImageService(service.BaseImageService):
"""Sets the client's auth token."""
self.client.set_auth_token(context.auth_token)
- def index(self, context, filters=None, marker=None, limit=None):
+ def index(self, context, **kwargs):
"""Calls out to Glance for a list of images available."""
- # NOTE(sirp): We need to use `get_images_detailed` and not
- # `get_images` here because we need `is_public` and `properties`
- # included so we can filter by user
- self._set_client_context(context)
- filtered = []
- filters = filters or {}
- if 'is_public' not in filters:
- # NOTE(vish): don't filter out private images
- filters['is_public'] = 'none'
- image_metas = self.client.get_images_detailed(filters=filters,
- marker=marker,
- limit=limit)
+ params = self._extract_query_params(kwargs)
+ image_metas = self._get_images(context, **params)
+
+ images = []
for image_meta in image_metas:
+ # NOTE(sirp): We need to use `get_images_detailed` and not
+ # `get_images` here because we need `is_public` and `properties`
+ # included so we can filter by user
if self._is_image_available(context, image_meta):
meta_subset = utils.subset_dict(image_meta, ('id', 'name'))
- filtered.append(meta_subset)
- return filtered
+ images.append(meta_subset)
+ return images
- def detail(self, context, filters=None, marker=None, limit=None):
+ def detail(self, context, **kwargs):
"""Calls out to Glance for a list of detailed image information."""
- self._set_client_context(context)
- filtered = []
- filters = filters or {}
- if 'is_public' not in filters:
- # NOTE(vish): don't filter out private images
- filters['is_public'] = 'none'
- image_metas = self.client.get_images_detailed(filters=filters,
- marker=marker,
- limit=limit)
+ params = self._extract_query_params(kwargs)
+ image_metas = self._get_images(context, **params)
+
+ images = []
for image_meta in image_metas:
if self._is_image_available(context, image_meta):
base_image_meta = self._translate_to_base(image_meta)
- filtered.append(base_image_meta)
- return filtered
+ images.append(base_image_meta)
+ return images
+
+ def _extract_query_params(self, params):
+ _params = {}
+ accepted_params = ('filters', 'marker', 'limit',
+ 'sort_key', 'sort_dir')
+ for param in accepted_params:
+ if param in params:
+ _params[param] = params.get(param)
+
+ return _params
+
+ def _get_images(self, context, **kwargs):
+ """Get image entitites from images service"""
+ self._set_client_context(context)
+
+ # ensure filters is a dict
+ kwargs['filters'] = kwargs.get('filters') or {}
+ # NOTE(vish): don't filter out private images
+ kwargs['filters'].setdefault('is_public', 'none')
+
+ return self._fetch_images(self.client.get_images_detailed, **kwargs)
+
+ def _fetch_images(self, fetch_func, **kwargs):
+ """Paginate through results from glance server"""
+ images = fetch_func(**kwargs)
+
+ for image in images:
+ yield image
+ else:
+ # break out of recursive loop to end pagination
+ return
+
+ try:
+ # attempt to advance the marker in order to fetch next page
+ kwargs['marker'] = images[-1]['id']
+ except KeyError:
+ raise exception.ImagePaginationFailed()
+
+ self._fetch_images(fetch_func, **kwargs)
def show(self, context, image_id):
"""Returns a dict with image data for the given opaque image id."""
@@ -194,6 +225,7 @@ class GlanceImageService(service.BaseImageService):
self._set_client_context(context)
# NOTE(vish): show is to check if image is available
self.show(context, image_id)
+ image_meta = _convert_to_string(image_meta)
try:
image_meta = self.client.update_image(image_id, image_meta, data)
except glance_exception.NotFound:
@@ -222,11 +254,19 @@ class GlanceImageService(service.BaseImageService):
pass
@classmethod
+ def _translate_to_service(cls, image_meta):
+ image_meta = super(GlanceImageService,
+ cls)._translate_to_service(image_meta)
+ image_meta = _convert_to_string(image_meta)
+ return image_meta
+
+ @classmethod
def _translate_to_base(cls, image_meta):
"""Override translation to handle conversion to datetime objects."""
image_meta = service.BaseImageService._propertify_metadata(
image_meta, cls.SERVICE_IMAGE_ATTRS)
image_meta = _convert_timestamps_to_datetimes(image_meta)
+ image_meta = _convert_from_string(image_meta)
return image_meta
@@ -252,3 +292,38 @@ def _parse_glance_iso8601_timestamp(timestamp):
raise ValueError(_('%(timestamp)s does not follow any of the '
'signatures: %(ISO_FORMATS)s') % locals())
+
+
+# TODO(yamahata): use block-device-mapping extension to glance
+def _json_loads(properties, attr):
+ prop = properties[attr]
+ if isinstance(prop, basestring):
+ properties[attr] = json.loads(prop)
+
+
+def _json_dumps(properties, attr):
+ prop = properties[attr]
+ if not isinstance(prop, basestring):
+ properties[attr] = json.dumps(prop)
+
+
+_CONVERT_PROPS = ('block_device_mapping', 'mappings')
+
+
+def _convert(method, metadata):
+ metadata = copy.deepcopy(metadata) # don't touch original metadata
+ properties = metadata.get('properties')
+ if properties:
+ for attr in _CONVERT_PROPS:
+ if attr in properties:
+ method(properties, attr)
+
+ return metadata
+
+
+def _convert_from_string(metadata):
+ return _convert(_json_loads, metadata)
+
+
+def _convert_to_string(metadata):
+ return _convert(_json_dumps, metadata)
diff --git a/nova/network/linux_net.py b/nova/network/linux_net.py
index 17b63a849..be4269392 100644
--- a/nova/network/linux_net.py
+++ b/nova/network/linux_net.py
@@ -301,14 +301,14 @@ class IptablesManager(object):
for cmd, tables in s:
for table in tables:
- current_table, _ = self.execute('sudo',
- '%s-save' % (cmd,),
+ current_table, _ = self.execute('%s-save' % (cmd,),
'-t', '%s' % (table,),
+ run_as_root=True,
attempts=5)
current_lines = current_table.split('\n')
new_filter = self._modify_rules(current_lines,
tables[table])
- self.execute('sudo', '%s-restore' % (cmd,),
+ self.execute('%s-restore' % (cmd,), run_as_root=True,
process_input='\n'.join(new_filter),
attempts=5)
@@ -401,21 +401,22 @@ def init_host():
def bind_floating_ip(floating_ip, check_exit_code=True):
"""Bind ip to public interface."""
- _execute('sudo', 'ip', 'addr', 'add', floating_ip,
+ _execute('ip', 'addr', 'add', floating_ip,
'dev', FLAGS.public_interface,
- check_exit_code=check_exit_code)
+ run_as_root=True, check_exit_code=check_exit_code)
def unbind_floating_ip(floating_ip):
"""Unbind a public ip from public interface."""
- _execute('sudo', 'ip', 'addr', 'del', floating_ip,
- 'dev', FLAGS.public_interface)
+ _execute('ip', 'addr', 'del', floating_ip,
+ 'dev', FLAGS.public_interface, run_as_root=True)
def ensure_metadata_ip():
"""Sets up local metadata ip."""
- _execute('sudo', 'ip', 'addr', 'add', '169.254.169.254/32',
- 'scope', 'link', 'dev', 'lo', check_exit_code=False)
+ _execute('ip', 'addr', 'add', '169.254.169.254/32',
+ 'scope', 'link', 'dev', 'lo',
+ run_as_root=True, check_exit_code=False)
def ensure_vpn_forward(public_ip, port, private_ip):
@@ -462,28 +463,29 @@ def initialize_gateway_device(dev, network_ref):
# NOTE(vish): The ip for dnsmasq has to be the first address on the
# bridge for it to respond to reqests properly
- suffix = network_ref['cidr'].rpartition('/')[2]
- out, err = _execute('sudo', 'ip', 'addr', 'add',
+ suffix = net_attrs['cidr'].rpartition('/')[2]
+ out, err = _execute('ip', 'addr', 'add',
'%s/%s' %
(network_ref['dhcp_server'], suffix),
'brd',
network_ref['broadcast'],
'dev',
dev,
+ run_as_root=True,
check_exit_code=False)
if err and err != 'RTNETLINK answers: File exists\n':
raise exception.Error('Failed to add ip: %s' % err)
if(FLAGS.use_ipv6):
- _execute('sudo', 'ip', '-f', 'inet6', 'addr',
- 'change', network_ref['cidr_v6'],
- 'dev', dev)
+ _execute('ip', '-f', 'inet6', 'addr',
+ 'change', net_attrs['cidr_v6'],
+ 'dev', dev, run_as_root=True)
# NOTE(vish): If the public interface is the same as the
# bridge, then the bridge has to be in promiscuous
# to forward packets properly.
if(FLAGS.public_interface == dev):
- _execute('sudo', 'ip', 'link', 'set',
- 'dev', dev, 'promisc', 'on')
- _execute('sudo', 'ip', 'link', 'set', dev, 'up')
+ _execute('ip', 'link', 'set',
+ 'dev', dev, 'promisc', 'on', run_as_root=True)
+ _execute('ip', 'link', 'set', dev, 'up', run_as_root=True)
def get_dhcp_leases(context, network_ref):
@@ -536,18 +538,33 @@ def update_dhcp(context, dev, network_ref):
check_exit_code=False)
if conffile in out:
try:
- _execute('sudo', 'kill', '-HUP', pid)
+ _execute('kill', '-HUP', pid, run_as_root=True)
return
except Exception as exc: # pylint: disable=W0703
LOG.debug(_('Hupping dnsmasq threw %s'), exc)
else:
LOG.debug(_('Pid %d is stale, relaunching dnsmasq'), pid)
- # FLAGFILE and NETWORK_ID in env
- env = {'FLAGFILE': FLAGS.dhcpbridge_flagfile,
- 'NETWORK_ID': str(network_ref['id'])}
- command = _dnsmasq_cmd(dev, network_ref)
- _execute(*command, addl_env=env)
+ cmd = ['FLAGFILE=%s' % FLAGS.dhcpbridge_flagfile,
+ 'NETWORK_ID=%s' % str(network_ref['id']),
+ 'dnsmasq',
+ '--strict-order',
+ '--bind-interfaces',
+ '--interface=%s' % dev,
+ '--conf-file=%s' % FLAGS.dnsmasq_config_file,
+ '--domain=%s' % FLAGS.dhcp_domain,
+ '--pid-file=%s' % _dhcp_file(dev, 'pid'),
+ '--listen-address=%s' % network_ref['dhcp_server'],
+ '--except-interface=lo',
+ '--dhcp-range=%s,static,120s' % network_ref['dhcp_start'],
+ '--dhcp-lease-max=%s' % len(netaddr.IPNetwork(network_ref['cidr'])),
+ '--dhcp-hostsfile=%s' % _dhcp_file(dev, 'conf'),
+ '--dhcp-script=%s' % FLAGS.dhcpbridge,
+ '--leasefile-ro']
+ if FLAGS.dns_server:
+ cmd += ['-h', '-R', '--server=%s' % FLAGS.dns_server]
+
+ _execute(*cmd, run_as_root=True)
@utils.synchronized('radvd_start')
@@ -580,13 +597,17 @@ interface %s
% pid, check_exit_code=False)
if conffile in out:
try:
- _execute('sudo', 'kill', pid)
+ _execute('kill', pid, run_as_root=True)
except Exception as exc: # pylint: disable=W0703
LOG.debug(_('killing radvd threw %s'), exc)
else:
LOG.debug(_('Pid %d is stale, relaunching radvd'), pid)
- command = _ra_cmd(dev)
- _execute(*command)
+
+ cmd = ['radvd',
+ '-C', '%s' % _ra_file(dev, 'conf'),
+ '-p', '%s' % _ra_file(dev, 'pid')]
+
+ _execute(*cmd, run_as_root=True)
def _host_lease(fixed_ip_ref):
@@ -630,43 +651,13 @@ def _device_exists(device):
return not err
-def _dnsmasq_cmd(dev, net):
- """Builds dnsmasq command."""
- cmd = ['sudo', '-E', 'dnsmasq',
- '--strict-order',
- '--bind-interfaces',
- '--interface=%s' % dev,
- '--conf-file=%s' % FLAGS.dnsmasq_config_file,
- '--domain=%s' % FLAGS.dhcp_domain,
- '--pid-file=%s' % _dhcp_file(dev, 'pid'),
- '--listen-address=%s' % net['dhcp_server'],
- '--except-interface=lo',
- '--dhcp-range=%s,static,120s' % net['dhcp_start'],
- '--dhcp-lease-max=%s' % len(netaddr.IPNetwork(net['cidr'])),
- '--dhcp-hostsfile=%s' % _dhcp_file(dev, 'conf'),
- '--dhcp-script=%s' % FLAGS.dhcpbridge,
- '--leasefile-ro']
- if FLAGS.dns_server:
- cmd += ['-h', '-R', '--server=%s' % FLAGS.dns_server]
- return cmd
-
-
-def _ra_cmd(dev):
- """Builds radvd command."""
- cmd = ['sudo', '-E', 'radvd',
-# '-u', 'nobody',
- '-C', '%s' % _ra_file(dev, 'conf'),
- '-p', '%s' % _ra_file(dev, 'pid')]
- return cmd
-
-
def _stop_dnsmasq(dev):
"""Stops the dnsmasq instance for a given network."""
pid = _dnsmasq_pid_for(dev)
if pid:
try:
- _execute('sudo', 'kill', '-TERM', pid)
+ _execute('kill', '-TERM', pid, run_as_root=True)
except Exception as exc: # pylint: disable=W0703
LOG.debug(_('Killing dnsmasq threw %s'), exc)
@@ -722,7 +713,7 @@ def _ra_pid_for(dev):
def _ip_bridge_cmd(action, params, device):
"""Build commands to add/del ips to bridges/devices."""
- cmd = ['sudo', 'ip', 'addr', action]
+ cmd = ['ip', 'addr', action]
cmd.extend(params)
cmd.extend(['dev', device])
return cmd
@@ -792,10 +783,11 @@ class LinuxBridgeInterfaceDriver(LinuxNetInterfaceDriver):
interface = 'vlan%s' % vlan_num
if not _device_exists(interface):
LOG.debug(_('Starting VLAN inteface %s'), interface)
- _execute('sudo', 'vconfig', 'set_name_type',
- 'VLAN_PLUS_VID_NO_PAD')
- _execute('sudo', 'vconfig', 'add', bridge_interface, vlan_num)
- _execute('sudo', 'ip', 'link', 'set', interface, 'up')
+ _execute('vconfig', 'set_name_type',
+ 'VLAN_PLUS_VID_NO_PAD', run_as_root=True)
+ _execute('vconfig', 'add', bridge_interface,
+ vlan_num, run_as_root=True)
+ _execute('ip', 'link', 'set', interface, 'up', run_as_root=True)
return interface
@classmethod
@@ -816,28 +808,29 @@ class LinuxBridgeInterfaceDriver(LinuxNetInterfaceDriver):
"""
if not _device_exists(bridge):
LOG.debug(_('Starting Bridge interface for %s'), interface)
- _execute('sudo', 'brctl', 'addbr', bridge)
- _execute('sudo', 'brctl', 'setfd', bridge, 0)
- # _execute('sudo brctl setageing %s 10' % bridge)
- _execute('sudo', 'brctl', 'stp', bridge, 'off')
+ _execute('brctl', 'addbr', bridge, run_as_root=True)
+ _execute('brctl', 'setfd', bridge, 0, run_as_root=True)
+ # _execute('brctl setageing %s 10' % bridge, run_as_root=True)
+ _execute('brctl', 'stp', bridge, 'off', run_as_root=True)
if interface:
- out, err = _execute('sudo', 'brctl', 'addif', bridge, interface,
- check_exit_code=False)
+ out, err = _execute('brctl', 'addif', bridge, interface,
+ check_exit_code=False, run_as_root=True)
# NOTE(vish): This will break if there is already an ip on the
# interface, so we move any ips to the bridge
gateway = None
- out, err = _execute('sudo', 'route', '-n')
+ out, err = _execute('route', '-n', run_as_root=True)
for line in out.split('\n'):
fields = line.split()
if fields and fields[0] == '0.0.0.0' and \
fields[-1] == interface:
gateway = fields[1]
- _execute('sudo', 'route', 'del', 'default', 'gw', gateway,
- 'dev', interface, check_exit_code=False)
- out, err = _execute('sudo', 'ip', 'addr', 'show', 'dev', interface,
- 'scope', 'global')
+ _execute('route', 'del', 'default', 'gw', gateway,
+ 'dev', interface, check_exit_code=False,
+ run_as_root=True)
+ out, err = _execute('ip', 'addr', 'show', 'dev', interface,
+ 'scope', 'global', run_as_root=True)
for line in out.split('\n'):
fields = line.split()
if fields and fields[0] == 'inet':
@@ -845,7 +838,8 @@ class LinuxBridgeInterfaceDriver(LinuxNetInterfaceDriver):
_execute(*_ip_bridge_cmd('del', params, fields[-1]))
_execute(*_ip_bridge_cmd('add', params, bridge))
if gateway:
- _execute('sudo', 'route', 'add', 'default', 'gw', gateway)
+ _execute('route', 'add', 'default', 'gw', gateway,
+ run_as_root=True)
if (err and err != "device %s is already a member of a bridge;"
"can't enslave it to bridge %s.\n" % (interface, bridge)):
@@ -867,7 +861,7 @@ class LinuxOVSInterfaceDriver(LinuxNetInterfaceDriver):
if not _device_exists(dev):
bridge = FLAGS.linuxnet_ovs_integration_bridge
mac_addr = utils.generate_mac_address()
- _execute('sudo', 'ovs-vsctl',
+ _execute('ovs-vsctl',
'--', '--may-exist', 'add-port', bridge, dev,
'--', 'set', 'Interface', dev, "type=internal",
'--', 'set', 'Interface', dev,
@@ -875,9 +869,10 @@ class LinuxOVSInterfaceDriver(LinuxNetInterfaceDriver):
'--', 'set', 'Interface', dev,
"external-ids:iface-status=active",
'--', 'set', 'Interface', dev,
- "external-ids:attached-mac=%s" % mac_addr)
- _execute('sudo', 'ip', 'link', 'set',
- dev, "address", mac_addr)
+ "external-ids:attached-mac=%s" % mac_addr,
+ run_as_root=True)
+ _execute('ip', 'link', 'set', dev, "address", mac_addr,
+ run_as_root=True)
return dev
def unplug(self, network):
diff --git a/nova/objectstore/s3server.py b/nova/objectstore/s3server.py
index 76025a1e3..1ab47b034 100644
--- a/nova/objectstore/s3server.py
+++ b/nova/objectstore/s3server.py
@@ -155,7 +155,10 @@ class BaseRequestHandler(object):
self.finish('<?xml version="1.0" encoding="UTF-8"?>\n' +
''.join(parts))
- def _render_parts(self, value, parts=[]):
+ def _render_parts(self, value, parts=None):
+ if not parts:
+ parts = []
+
if isinstance(value, basestring):
parts.append(utils.xhtml_escape(value))
elif isinstance(value, int) or isinstance(value, long):
diff --git a/nova/scheduler/api.py b/nova/scheduler/api.py
index 137b671c0..55cea5f8f 100644
--- a/nova/scheduler/api.py
+++ b/nova/scheduler/api.py
@@ -17,7 +17,8 @@
Handles all requests relating to schedulers.
"""
-import novaclient
+from novaclient import v1_1 as novaclient
+from novaclient import exceptions as novaclient_exceptions
from nova import db
from nova import exception
@@ -112,7 +113,7 @@ def _wrap_method(function, self):
def _process(func, zone):
"""Worker stub for green thread pool. Give the worker
an authenticated nova client and zone info."""
- nova = novaclient.OpenStack(zone.username, zone.password, None,
+ nova = novaclient.Client(zone.username, zone.password, None,
zone.api_url)
nova.authenticate()
return func(nova, zone)
@@ -132,10 +133,10 @@ def call_zone_method(context, method_name, errors_to_ignore=None,
zones = db.zone_get_all(context)
for zone in zones:
try:
- nova = novaclient.OpenStack(zone.username, zone.password, None,
+ nova = novaclient.Client(zone.username, zone.password, None,
zone.api_url)
nova.authenticate()
- except novaclient.exceptions.BadRequest, e:
+ except novaclient_exceptions.BadRequest, e:
url = zone.api_url
LOG.warn(_("Failed request to zone; URL=%(url)s: %(e)s")
% locals())
@@ -188,7 +189,7 @@ def _issue_novaclient_command(nova, zone, collection,
if method_name in ['find', 'findall']:
try:
return getattr(manager, method_name)(**kwargs)
- except novaclient.NotFound:
+ except novaclient_exceptions.NotFound:
url = zone.api_url
LOG.debug(_("%(collection)s.%(method_name)s didn't find "
"anything matching '%(kwargs)s' on '%(url)s'" %
@@ -200,7 +201,7 @@ def _issue_novaclient_command(nova, zone, collection,
item = args.pop(0)
try:
result = manager.get(item)
- except novaclient.NotFound:
+ except novaclient_exceptions.NotFound:
url = zone.api_url
LOG.debug(_("%(collection)s '%(item)s' not found on '%(url)s'" %
locals()))
diff --git a/nova/scheduler/least_cost.py b/nova/scheduler/least_cost.py
index 8c400d476..329107efe 100644
--- a/nova/scheduler/least_cost.py
+++ b/nova/scheduler/least_cost.py
@@ -96,7 +96,8 @@ class LeastCostScheduler(zone_aware_scheduler.ZoneAwareScheduler):
cost_fn_str=cost_fn_str)
try:
- weight = getattr(FLAGS, "%s_weight" % cost_fn.__name__)
+ flag_name = "%s_weight" % cost_fn.__name__
+ weight = getattr(FLAGS, flag_name)
except AttributeError:
raise exception.SchedulerWeightFlagNotFound(
flag_name=flag_name)
diff --git a/nova/scheduler/manager.py b/nova/scheduler/manager.py
index 749d66cad..c8b16b622 100644
--- a/nova/scheduler/manager.py
+++ b/nova/scheduler/manager.py
@@ -69,8 +69,10 @@ class SchedulerManager(manager.Manager):
return self.zone_manager.get_zone_capabilities(context)
def update_service_capabilities(self, context=None, service_name=None,
- host=None, capabilities={}):
+ host=None, capabilities=None):
"""Process a capability update from a service node."""
+ if not capability:
+ capability = {}
self.zone_manager.update_service_capabilities(service_name,
host, capabilities)
diff --git a/nova/scheduler/zone_aware_scheduler.py b/nova/scheduler/zone_aware_scheduler.py
index d99d7214c..d1924c9f9 100644
--- a/nova/scheduler/zone_aware_scheduler.py
+++ b/nova/scheduler/zone_aware_scheduler.py
@@ -24,7 +24,9 @@ import operator
import json
import M2Crypto
-import novaclient
+
+from novaclient import v1_1 as novaclient
+from novaclient import exceptions as novaclient_exceptions
from nova import crypto
from nova import db
@@ -58,12 +60,13 @@ class ZoneAwareScheduler(driver.Scheduler):
"""Create the requested resource in this Zone."""
host = build_plan_item['hostname']
base_options = request_spec['instance_properties']
+ image = request_spec['image']
# TODO(sandy): I guess someone needs to add block_device_mapping
# support at some point? Also, OS API has no concept of security
# groups.
instance = compute_api.API().create_db_entry_for_new_instance(context,
- base_options, None, [])
+ image, base_options, None, [])
instance_id = instance['id']
kwargs['instance_id'] = instance_id
@@ -117,10 +120,9 @@ class ZoneAwareScheduler(driver.Scheduler):
% locals())
nova = None
try:
- nova = novaclient.OpenStack(zone.username, zone.password, None,
- url)
+ nova = novaclient.Client(zone.username, zone.password, None, url)
nova.authenticate()
- except novaclient.exceptions.BadRequest, e:
+ except novaclient_exceptions.BadRequest, e:
raise exception.NotAuthorized(_("Bad credentials attempting "
"to talk to zone at %(url)s.") % locals())
@@ -264,8 +266,8 @@ class ZoneAwareScheduler(driver.Scheduler):
"""
if topic != "compute":
- raise NotImplemented(_("Zone Aware Scheduler only understands "
- "Compute nodes (for now)"))
+ raise NotImplementedError(_("Zone Aware Scheduler only understands"
+ " Compute nodes (for now)"))
num_instances = request_spec.get('num_instances', 1)
instance_type = request_spec['instance_type']
diff --git a/nova/scheduler/zone_manager.py b/nova/scheduler/zone_manager.py
index efdac06e1..9d05ea42e 100644
--- a/nova/scheduler/zone_manager.py
+++ b/nova/scheduler/zone_manager.py
@@ -18,10 +18,11 @@ ZoneManager oversees all communications with child Zones.
"""
import datetime
-import novaclient
import thread
import traceback
+from novaclient import v1_1 as novaclient
+
from eventlet import greenpool
from nova import db
@@ -89,8 +90,8 @@ class ZoneState(object):
def _call_novaclient(zone):
"""Call novaclient. Broken out for testing purposes."""
- client = novaclient.OpenStack(zone.username, zone.password, None,
- zone.api_url)
+ client = novaclient.Client(zone.username, zone.password, None,
+ zone.api_url)
return client.zones.info()._info
@@ -197,7 +198,7 @@ class ZoneManager(object):
def update_service_capabilities(self, service_name, host, capabilities):
"""Update the per-service capabilities based on this notification."""
logging.debug(_("Received %(service_name)s service update from "
- "%(host)s: %(capabilities)s") % locals())
+ "%(host)s.") % locals())
service_caps = self.service_states.get(host, {})
capabilities["timestamp"] = utils.utcnow() # Reported time
service_caps[service_name] = capabilities
diff --git a/nova/test.py b/nova/test.py
index 549aa6fcf..88f1489e8 100644
--- a/nova/test.py
+++ b/nova/test.py
@@ -60,11 +60,42 @@ class skip_test(object):
self.message = msg
def __call__(self, func):
+ @functools.wraps(func)
def _skipper(*args, **kw):
"""Wrapped skipper function."""
raise nose.SkipTest(self.message)
- _skipper.__name__ = func.__name__
- _skipper.__doc__ = func.__doc__
+ return _skipper
+
+
+class skip_if(object):
+ """Decorator that skips a test if contition is true."""
+ def __init__(self, condition, msg):
+ self.condition = condition
+ self.message = msg
+
+ def __call__(self, func):
+ @functools.wraps(func)
+ def _skipper(*args, **kw):
+ """Wrapped skipper function."""
+ if self.condition:
+ raise nose.SkipTest(self.message)
+ func(*args, **kw)
+ return _skipper
+
+
+class skip_unless(object):
+ """Decorator that skips a test if condition is not true."""
+ def __init__(self, condition, msg):
+ self.condition = condition
+ self.message = msg
+
+ def __call__(self, func):
+ @functools.wraps(func)
+ def _skipper(*args, **kw):
+ """Wrapped skipper function."""
+ if not self.condition:
+ raise nose.SkipTest(self.message)
+ func(*args, **kw)
return _skipper
@@ -141,11 +172,9 @@ class TestCase(unittest.TestCase):
def flags(self, **kw):
"""Override flag variables for a test."""
for k, v in kw.iteritems():
- if k in self.flag_overrides:
- self.reset_flags()
- raise Exception(
- 'trying to override already overriden flag: %s' % k)
- self.flag_overrides[k] = getattr(FLAGS, k)
+ # Store original flag value if it's not been overriden yet
+ if k not in self.flag_overrides:
+ self.flag_overrides[k] = getattr(FLAGS, k)
setattr(FLAGS, k, v)
def reset_flags(self):
diff --git a/nova/tests/api/openstack/__init__.py b/nova/tests/api/openstack/__init__.py
index bfb424afe..458434a81 100644
--- a/nova/tests/api/openstack/__init__.py
+++ b/nova/tests/api/openstack/__init__.py
@@ -22,14 +22,11 @@ import webob.dec
from nova import test
from nova import context
-from nova import flags
from nova.api.openstack.limits import RateLimitingMiddleware
from nova.api.openstack.common import limited
from nova.tests.api.openstack import fakes
from webob import Request
-FLAGS = flags.FLAGS
-
@webob.dec.wsgify
def simple_wsgi(req):
diff --git a/nova/tests/api/openstack/contrib/test_keypairs.py b/nova/tests/api/openstack/contrib/test_keypairs.py
new file mode 100644
index 000000000..c9dc34d65
--- /dev/null
+++ b/nova/tests/api/openstack/contrib/test_keypairs.py
@@ -0,0 +1,99 @@
+# Copyright 2011 Eldar Nugaev
+# 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 json
+import webob
+
+from nova import context
+from nova import db
+from nova import test
+from nova.api.openstack.contrib.keypairs import KeypairController
+from nova.tests.api.openstack import fakes
+
+
+def fake_keypair(name):
+ return {'public_key': 'FAKE_KEY',
+ 'fingerprint': 'FAKE_FINGERPRINT',
+ 'name': name}
+
+def db_key_pair_get_all_by_user(self, user_id):
+ return [fake_keypair('FAKE')]
+
+
+def db_key_pair_create(self, keypair):
+ pass
+
+
+def db_key_pair_destroy(context, user_id, name):
+ if not (user_id and name):
+ raise Exception()
+
+
+class KeypairsTest(test.TestCase):
+
+ def setUp(self):
+ super(KeypairsTest, self).setUp()
+ self.controller = KeypairController()
+ fakes.stub_out_networking(self.stubs)
+ fakes.stub_out_rate_limiting(self.stubs)
+ self.stubs.Set(db, "key_pair_get_all_by_user",
+ db_key_pair_get_all_by_user)
+ self.stubs.Set(db, "key_pair_create",
+ db_key_pair_create)
+ self.stubs.Set(db, "key_pair_destroy",
+ db_key_pair_destroy)
+ self.context = context.get_admin_context()
+
+ def test_keypair_list(self):
+ req = webob.Request.blank('/v1.1/os-keypairs')
+ res = req.get_response(fakes.wsgi_app())
+ self.assertEqual(res.status_int, 200)
+ res_dict = json.loads(res.body)
+ response = {'keypairs': [{'keypair': fake_keypair('FAKE')}]}
+ self.assertEqual(res_dict, response)
+
+ def test_keypair_create(self):
+ body = {'keypair': {'name': 'create_test'}}
+ req = webob.Request.blank('/v1.1/os-keypairs')
+ req.method = 'POST'
+ req.body = json.dumps(body)
+ req.headers['Content-Type'] = 'application/json'
+ res = req.get_response(fakes.wsgi_app())
+ self.assertEqual(res.status_int, 200)
+ res_dict = json.loads(res.body)
+ self.assertTrue(len(res_dict['keypair']['fingerprint']) > 0)
+ self.assertTrue(len(res_dict['keypair']['private_key']) > 0)
+
+ def test_keypair_import(self):
+ body = {'keypair': {'name': 'create_test',
+ 'public_key': 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDBYIznAx9D7118Q1VKGpXy2HDiKyUTM8XcUuhQpo0srqb9rboUp4a9NmCwpWpeElDLuva707GOUnfaBAvHBwsRXyxHJjRaI6YQj2oLJwqvaSaWUbyT1vtryRqy6J3TecN0WINY71f4uymiMZP0wby4bKBcYnac8KiCIlvkEl0ETjkOGUq8OyWRmn7ljj5SESEUdBP0JnuTFKddWTU/wD6wydeJaUhBTqOlHn0kX1GyqoNTE1UEhcM5ZRWgfUZfTjVyDF2kGj3vJLCJtJ8LoGcj7YaN4uPg1rBle+izwE/tLonRrds+cev8p6krSSrxWOwBbHkXa6OciiJDvkRzJXzf'}}
+ req = webob.Request.blank('/v1.1/os-keypairs')
+ req.method = 'POST'
+ req.body = json.dumps(body)
+ req.headers['Content-Type'] = 'application/json'
+ res = req.get_response(fakes.wsgi_app())
+ self.assertEqual(res.status_int, 200)
+ # FIXME(ja): sholud we check that public_key was sent to create?
+ res_dict = json.loads(res.body)
+ self.assertTrue(len(res_dict['keypair']['fingerprint']) > 0)
+ self.assertFalse('private_key' in res_dict['keypair'])
+
+ def test_keypair_delete(self):
+ req = webob.Request.blank('/v1.1/os-keypairs/FAKE')
+ req.method = 'DELETE'
+ req.headers['Content-Type'] = 'application/json'
+ res = req.get_response(fakes.wsgi_app())
+ self.assertEqual(res.status_int, 202)
+
diff --git a/nova/tests/api/openstack/fakes.py b/nova/tests/api/openstack/fakes.py
index a67a28a4e..d11fbf788 100644
--- a/nova/tests/api/openstack/fakes.py
+++ b/nova/tests/api/openstack/fakes.py
@@ -71,14 +71,18 @@ def fake_wsgi(self, req):
return self.application
-def wsgi_app(inner_app10=None, inner_app11=None, fake_auth=True):
+def wsgi_app(inner_app10=None, inner_app11=None, fake_auth=True,
+ fake_auth_context=None):
if not inner_app10:
inner_app10 = openstack.APIRouterV10()
if not inner_app11:
inner_app11 = openstack.APIRouterV11()
if fake_auth:
- ctxt = context.RequestContext('fake', 'fake')
+ if fake_auth_context is not None:
+ ctxt = fake_auth_context
+ else:
+ ctxt = context.RequestContext('fake', 'fake')
api10 = openstack.FaultWrapper(wsgi.InjectContext(ctxt,
limits.RateLimitingMiddleware(inner_app10)))
api11 = openstack.FaultWrapper(wsgi.InjectContext(ctxt,
diff --git a/nova/tests/api/openstack/test_accounts.py b/nova/tests/api/openstack/test_accounts.py
index 89dbf5213..707a2599f 100644
--- a/nova/tests/api/openstack/test_accounts.py
+++ b/nova/tests/api/openstack/test_accounts.py
@@ -18,17 +18,12 @@ import json
import webob
-from nova import flags
from nova import test
from nova.api.openstack import accounts
from nova.auth.manager import User
from nova.tests.api.openstack import fakes
-FLAGS = flags.FLAGS
-FLAGS.verbose = True
-
-
def fake_init(self):
self.manager = fakes.FakeAuthManager()
@@ -40,7 +35,7 @@ def fake_admin_check(self, req):
class AccountsTest(test.TestCase):
def setUp(self):
super(AccountsTest, self).setUp()
- self.flags(allow_admin_api=True)
+ self.flags(verbose=True, allow_admin_api=True)
self.stubs.Set(accounts.Controller, '__init__',
fake_init)
self.stubs.Set(accounts.Controller, '_check_admin',
diff --git a/nova/tests/api/openstack/test_adminapi.py b/nova/tests/api/openstack/test_adminapi.py
index b83de40cf..c9e66dc4c 100644
--- a/nova/tests/api/openstack/test_adminapi.py
+++ b/nova/tests/api/openstack/test_adminapi.py
@@ -18,12 +18,9 @@
import webob
-from nova import flags
from nova import test
from nova.tests.api.openstack import fakes
-FLAGS = flags.FLAGS
-
class AdminAPITest(test.TestCase):
@@ -31,7 +28,7 @@ class AdminAPITest(test.TestCase):
super(AdminAPITest, self).setUp()
fakes.stub_out_networking(self.stubs)
fakes.stub_out_rate_limiting(self.stubs)
- self.allow_admin = FLAGS.allow_admin_api
+ self.flags(verbose=True)
def test_admin_enabled(self):
self.flags(allow_admin_api=True)
diff --git a/nova/tests/api/openstack/test_common.py b/nova/tests/api/openstack/test_common.py
index 0b76841f0..5a6e43579 100644
--- a/nova/tests/api/openstack/test_common.py
+++ b/nova/tests/api/openstack/test_common.py
@@ -323,14 +323,10 @@ class MetadataXMLSerializationTest(test.TestCase):
expected = minidom.parseString("""
<metadata xmlns="http://docs.openstack.org/compute/api/v1.1">
- <meta key="three">
- four
- </meta>
- <meta key="one">
- two
- </meta>
+ <meta key="three">four</meta>
+ <meta key="one">two</meta>
</metadata>
- """.replace(" ", ""))
+ """.replace(" ", "").replace("\n", ""))
self.assertEqual(expected.toxml(), actual.toxml())
@@ -346,11 +342,9 @@ class MetadataXMLSerializationTest(test.TestCase):
expected = minidom.parseString("""
<metadata xmlns="http://docs.openstack.org/compute/api/v1.1">
- <meta key="None">
- None
- </meta>
+ <meta key="None">None</meta>
</metadata>
- """.replace(" ", ""))
+ """.replace(" ", "").replace("\n", ""))
self.assertEqual(expected.toxml(), actual.toxml())
@@ -366,11 +360,9 @@ class MetadataXMLSerializationTest(test.TestCase):
expected = minidom.parseString(u"""
<metadata xmlns="http://docs.openstack.org/compute/api/v1.1">
- <meta key="three">
- Jos\xe9
- </meta>
+ <meta key="three">Jos\xe9</meta>
</metadata>
- """.encode("UTF-8").replace(" ", ""))
+ """.encode("UTF-8").replace(" ", "").replace("\n", ""))
self.assertEqual(expected.toxml(), actual.toxml())
@@ -385,10 +377,9 @@ class MetadataXMLSerializationTest(test.TestCase):
actual = minidom.parseString(output.replace(" ", ""))
expected = minidom.parseString("""
- <meta xmlns="http://docs.openstack.org/compute/api/v1.1" key="one">
- two
- </meta>
- """.replace(" ", ""))
+ <meta xmlns="http://docs.openstack.org/compute/api/v1.1"
+ key="one">two</meta>
+ """.replace(" ", "").replace("\n", ""))
self.assertEqual(expected.toxml(), actual.toxml())
@@ -405,14 +396,10 @@ class MetadataXMLSerializationTest(test.TestCase):
expected = minidom.parseString("""
<metadata xmlns="http://docs.openstack.org/compute/api/v1.1">
- <meta key="key6">
- value6
- </meta>
- <meta key="key4">
- value4
- </meta>
+ <meta key="key6">value6</meta>
+ <meta key="key4">value4</meta>
</metadata>
- """.replace(" ", ""))
+ """.replace(" ", "").replace("\n", ""))
self.assertEqual(expected.toxml(), actual.toxml())
@@ -427,10 +414,9 @@ class MetadataXMLSerializationTest(test.TestCase):
actual = minidom.parseString(output.replace(" ", ""))
expected = minidom.parseString("""
- <meta xmlns="http://docs.openstack.org/compute/api/v1.1" key="one">
- two
- </meta>
- """.replace(" ", ""))
+ <meta xmlns="http://docs.openstack.org/compute/api/v1.1"
+ key="one">two</meta>
+ """.replace(" ", "").replace("\n", ""))
self.assertEqual(expected.toxml(), actual.toxml())
@@ -448,17 +434,11 @@ class MetadataXMLSerializationTest(test.TestCase):
expected = minidom.parseString("""
<metadata xmlns="http://docs.openstack.org/compute/api/v1.1">
- <meta key="key2">
- value2
- </meta>
- <meta key="key9">
- value9
- </meta>
- <meta key="key1">
- value1
- </meta>
+ <meta key="key2">value2</meta>
+ <meta key="key9">value9</meta>
+ <meta key="key1">value1</meta>
</metadata>
- """.replace(" ", ""))
+ """.replace(" ", "").replace("\n", ""))
self.assertEqual(expected.toxml(), actual.toxml())
diff --git a/nova/tests/api/openstack/test_extensions.py b/nova/tests/api/openstack/test_extensions.py
index 47c37225c..8b7e11a5b 100644
--- a/nova/tests/api/openstack/test_extensions.py
+++ b/nova/tests/api/openstack/test_extensions.py
@@ -18,18 +18,17 @@
import json
import os.path
import webob
-from xml.etree import ElementTree
+from lxml import etree
from nova import context
-from nova import flags
from nova import test
from nova.api import openstack
from nova.api.openstack import extensions
from nova.api.openstack import flavors
from nova.api.openstack import wsgi
+from nova.api.openstack import xmlutil
from nova.tests.api.openstack import fakes
-FLAGS = flags.FLAGS
NS = "{http://docs.openstack.org/compute/api/v1.1}"
ATOMNS = "{http://www.w3.org/2005/Atom}"
response_body = "Try to say this Mr. Knox, sir..."
@@ -98,7 +97,7 @@ class ExtensionControllerTest(test.TestCase):
names = [x['name'] for x in data['extensions']]
names.sort()
self.assertEqual(names, ["FlavorExtraSpecs", "Floating_ips",
- "Fox In Socks", "Hosts", "Multinic", "Volumes"])
+ "Fox In Socks", "Hosts", "Keypairs", "Multinic", "Volumes"])
# Make sure that at least Fox in Sox is correct.
(fox_ext,) = [
@@ -140,12 +139,12 @@ class ExtensionControllerTest(test.TestCase):
self.assertEqual(200, response.status_int)
print response.body
- root = ElementTree.XML(response.body)
+ root = etree.XML(response.body)
self.assertEqual(root.tag.split('extensions')[0], NS)
# Make sure we have all the extensions.
exts = root.findall('{0}extension'.format(NS))
- self.assertEqual(len(exts), 6)
+ self.assertEqual(len(exts), 7)
# Make sure that at least Fox in Sox is correct.
(fox_ext,) = [x for x in exts if x.get('alias') == 'FOXNSOX']
@@ -156,6 +155,8 @@ class ExtensionControllerTest(test.TestCase):
self.assertEqual(fox_ext.findtext('{0}description'.format(NS)),
'The Fox In Socks Extension')
+ xmlutil.validate_schema(root, 'extensions')
+
def test_get_extension_xml(self):
app = openstack.APIRouterV11()
ext_midware = extensions.ExtensionMiddleware(app)
@@ -163,9 +164,10 @@ class ExtensionControllerTest(test.TestCase):
request.accept = "application/xml"
response = request.get_response(ext_midware)
self.assertEqual(200, response.status_int)
- print response.body
+ xml = response.body
+ print xml
- root = ElementTree.XML(response.body)
+ root = etree.XML(xml)
self.assertEqual(root.tag.split('extension')[0], NS)
self.assertEqual(root.get('alias'), 'FOXNSOX')
self.assertEqual(root.get('name'), 'Fox In Socks')
@@ -175,6 +177,8 @@ class ExtensionControllerTest(test.TestCase):
self.assertEqual(root.findtext('{0}description'.format(NS)),
'The Fox In Socks Extension')
+ xmlutil.validate_schema(root, 'extension')
+
class ResourceExtensionTest(test.TestCase):
@@ -354,7 +358,8 @@ class ExtensionsXMLSerializerTest(test.TestCase):
}
xml = serializer.serialize(data, 'show')
- root = ElementTree.XML(xml)
+ print xml
+ root = etree.XML(xml)
ext_dict = data['extension']
self.assertEqual(root.findtext('{0}description'.format(NS)),
ext_dict['description'])
@@ -368,6 +373,8 @@ class ExtensionsXMLSerializerTest(test.TestCase):
for key, value in link.items():
self.assertEqual(link_nodes[i].get(key), value)
+ xmlutil.validate_schema(root, 'extension')
+
def test_serialize_extensions(self):
serializer = extensions.ExtensionsXMLSerializer()
data = {
@@ -415,7 +422,7 @@ class ExtensionsXMLSerializerTest(test.TestCase):
xml = serializer.serialize(data, 'index')
print xml
- root = ElementTree.XML(xml)
+ root = etree.XML(xml)
ext_elems = root.findall('{0}extension'.format(NS))
self.assertEqual(len(ext_elems), 2)
for i, ext_elem in enumerate(ext_elems):
@@ -431,3 +438,5 @@ class ExtensionsXMLSerializerTest(test.TestCase):
for i, link in enumerate(ext_dict['links']):
for key, value in link.items():
self.assertEqual(link_nodes[i].get(key), value)
+
+ xmlutil.validate_schema(root, 'extensions')
diff --git a/nova/tests/api/openstack/test_flavors_extra_specs.py b/nova/tests/api/openstack/test_flavors_extra_specs.py
index d386958db..ccd1b0d9f 100644
--- a/nova/tests/api/openstack/test_flavors_extra_specs.py
+++ b/nova/tests/api/openstack/test_flavors_extra_specs.py
@@ -21,15 +21,12 @@ import webob
import os.path
-from nova import flags
from nova import test
from nova.api import openstack
from nova.api.openstack import extensions
from nova.tests.api.openstack import fakes
import nova.wsgi
-FLAGS = flags.FLAGS
-
def return_create_flavor_extra_specs(context, flavor_id, extra_specs):
return stub_flavor_extra_specs()
diff --git a/nova/tests/api/openstack/test_images.py b/nova/tests/api/openstack/test_images.py
index 942c0b333..383ed2e03 100644
--- a/nova/tests/api/openstack/test_images.py
+++ b/nova/tests/api/openstack/test_images.py
@@ -34,7 +34,6 @@ import webob
from glance import client as glance_client
from nova import context
from nova import exception
-from nova import flags
from nova import test
from nova import utils
import nova.api.openstack
@@ -42,9 +41,6 @@ from nova.api.openstack import images
from nova.tests.api.openstack import fakes
-FLAGS = flags.FLAGS
-
-
class _BaseImageServiceTests(test.TestCase):
"""Tasks to test for all image services"""
@@ -328,8 +324,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
def setUp(self):
"""Run before each test."""
super(ImageControllerWithGlanceServiceTest, self).setUp()
- self.orig_image_service = FLAGS.image_service
- FLAGS.image_service = 'nova.image.glance.GlanceImageService'
+ self.flags(image_service='nova.image.glance.GlanceImageService')
self.stubs = stubout.StubOutForTesting()
fakes.stub_out_networking(self.stubs)
fakes.stub_out_rate_limiting(self.stubs)
@@ -342,7 +337,6 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
def tearDown(self):
"""Run after each test."""
self.stubs.UnsetAll()
- FLAGS.image_service = self.orig_image_service
super(ImageControllerWithGlanceServiceTest, self).tearDown()
def _applicable_fixture(self, fixture, user_id):
@@ -385,6 +379,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
"updated": self.NOW_API_FORMAT,
"created": self.NOW_API_FORMAT,
"status": "ACTIVE",
+ "progress": 100,
},
}
@@ -408,6 +403,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
"updated": self.NOW_API_FORMAT,
"created": self.NOW_API_FORMAT,
"status": "QUEUED",
+ "progress": 0,
'server': {
'id': 42,
"links": [{
@@ -450,6 +446,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
updated="%(expected_now)s"
created="%(expected_now)s"
status="ACTIVE"
+ progress="100"
xmlns="http://docs.rackspacecloud.com/servers/api/v1.0" />
""" % (locals()))
@@ -469,6 +466,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
updated="%(expected_now)s"
created="%(expected_now)s"
status="ACTIVE"
+ progress="100"
xmlns="http://docs.rackspacecloud.com/servers/api/v1.0" />
""" % (locals()))
@@ -593,6 +591,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
'updated': self.NOW_API_FORMAT,
'created': self.NOW_API_FORMAT,
'status': 'ACTIVE',
+ 'progress': 100,
},
{
'id': 124,
@@ -600,6 +599,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
'updated': self.NOW_API_FORMAT,
'created': self.NOW_API_FORMAT,
'status': 'QUEUED',
+ 'progress': 0,
},
{
'id': 125,
@@ -614,7 +614,8 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
'name': 'active snapshot',
'updated': self.NOW_API_FORMAT,
'created': self.NOW_API_FORMAT,
- 'status': 'ACTIVE'
+ 'status': 'ACTIVE',
+ 'progress': 100,
},
{
'id': 127,
@@ -622,6 +623,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
'updated': self.NOW_API_FORMAT,
'created': self.NOW_API_FORMAT,
'status': 'FAILED',
+ 'progress': 0,
},
{
'id': 129,
@@ -629,6 +631,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
'updated': self.NOW_API_FORMAT,
'created': self.NOW_API_FORMAT,
'status': 'ACTIVE',
+ 'progress': 100,
}]
self.assertDictListMatch(expected, response_list)
@@ -649,6 +652,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
'updated': self.NOW_API_FORMAT,
'created': self.NOW_API_FORMAT,
'status': 'ACTIVE',
+ 'progress': 100,
"links": [{
"rel": "self",
"href": "http://localhost/v1.1/images/123",
@@ -668,6 +672,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
'updated': self.NOW_API_FORMAT,
'created': self.NOW_API_FORMAT,
'status': 'QUEUED',
+ 'progress': 0,
'server': {
'id': 42,
"links": [{
@@ -729,6 +734,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
'updated': self.NOW_API_FORMAT,
'created': self.NOW_API_FORMAT,
'status': 'ACTIVE',
+ 'progress': 100,
'server': {
'id': 42,
"links": [{
@@ -759,6 +765,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
'updated': self.NOW_API_FORMAT,
'created': self.NOW_API_FORMAT,
'status': 'FAILED',
+ 'progress': 0,
'server': {
'id': 42,
"links": [{
@@ -786,6 +793,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
'updated': self.NOW_API_FORMAT,
'created': self.NOW_API_FORMAT,
'status': 'ACTIVE',
+ 'progress': 100,
"links": [{
"rel": "self",
"href": "http://localhost/v1.1/images/129",
@@ -1007,7 +1015,8 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
image_meta = json.loads(res.body)['image']
expected = {'id': 123, 'name': 'public image',
'updated': self.NOW_API_FORMAT,
- 'created': self.NOW_API_FORMAT, 'status': 'ACTIVE'}
+ 'created': self.NOW_API_FORMAT, 'status': 'ACTIVE',
+ 'progress': 100}
self.assertDictMatch(image_meta, expected)
def test_get_image_non_existent(self):
@@ -1031,6 +1040,9 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
req.headers["content-type"] = "application/json"
response = req.get_response(fakes.wsgi_app())
self.assertEqual(200, response.status_int)
+ image_meta = json.loads(response.body)['image']
+ self.assertEqual(123, image_meta['serverId'])
+ self.assertEqual('Snapshot 1', image_meta['name'])
def test_create_snapshot_no_name(self):
"""Name is required for snapshots"""
@@ -1052,6 +1064,16 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
response = req.get_response(fakes.wsgi_app())
self.assertEqual(400, response.status_int)
+ def test_create_image_snapshots_disabled(self):
+ self.flags(allow_instance_snapshots=False)
+ body = dict(image=dict(serverId='123', name='Snapshot 1'))
+ req = webob.Request.blank('/v1.0/images')
+ req.method = 'POST'
+ req.body = json.dumps(body)
+ req.headers["content-type"] = "application/json"
+ response = req.get_response(fakes.wsgi_app())
+ self.assertEqual(400, response.status_int)
+
@classmethod
def _make_image_fixtures(cls):
image_id = 123
diff --git a/nova/tests/api/openstack/test_limits.py b/nova/tests/api/openstack/test_limits.py
index 6c3d531e3..1dc3c3a17 100644
--- a/nova/tests/api/openstack/test_limits.py
+++ b/nova/tests/api/openstack/test_limits.py
@@ -819,12 +819,15 @@ class FakeHttplibConnection(object):
self.app = app
self.host = host
- def request(self, method, path, body="", headers={}):
+ def request(self, method, path, body="", headers=None):
"""
Requests made via this connection actually get translated and routed
into our WSGI app, we then wait for the response and turn it back into
an `httplib.HTTPResponse`.
"""
+ if not headers:
+ headers = {}
+
req = webob.Request.blank(path)
req.method = method
req.headers = headers
diff --git a/nova/tests/api/openstack/test_server_actions.py b/nova/tests/api/openstack/test_server_actions.py
new file mode 100644
index 000000000..717e11c00
--- /dev/null
+++ b/nova/tests/api/openstack/test_server_actions.py
@@ -0,0 +1,1075 @@
+import base64
+import json
+import unittest
+from xml.dom import minidom
+
+import stubout
+import webob
+
+from nova import context
+from nova import db
+from nova import utils
+from nova import flags
+from nova.api.openstack import create_instance_helper
+from nova.compute import instance_types
+from nova.compute import power_state
+import nova.db.api
+from nova import test
+from nova.tests.api.openstack import common
+from nova.tests.api.openstack import fakes
+
+
+FLAGS = flags.FLAGS
+
+
+def return_server_by_id(context, id):
+ return _get_instance()
+
+
+def instance_update(context, instance_id, kwargs):
+ return _get_instance()
+
+
+def return_server_with_power_state(power_state):
+ def _return_server(context, id):
+ instance = _get_instance()
+ instance['state'] = power_state
+ return instance
+ return _return_server
+
+
+def return_server_with_uuid_and_power_state(power_state):
+ def _return_server(context, id):
+ return return_server_with_power_state(power_state)
+ return _return_server
+
+
+class MockSetAdminPassword(object):
+ def __init__(self):
+ self.instance_id = None
+ self.password = None
+
+ def __call__(self, context, instance_id, password):
+ self.instance_id = instance_id
+ self.password = password
+
+
+def _get_instance():
+ instance = {
+ "id": 1,
+ "created_at": "2010-10-10 12:00:00",
+ "updated_at": "2010-11-11 11:00:00",
+ "admin_pass": "",
+ "user_id": "",
+ "project_id": "",
+ "image_ref": "5",
+ "kernel_id": "",
+ "ramdisk_id": "",
+ "launch_index": 0,
+ "key_name": "",
+ "key_data": "",
+ "state": 0,
+ "state_description": "",
+ "memory_mb": 0,
+ "vcpus": 0,
+ "local_gb": 0,
+ "hostname": "",
+ "host": "",
+ "instance_type": {
+ "flavorid": 1,
+ },
+ "user_data": "",
+ "reservation_id": "",
+ "mac_address": "",
+ "scheduled_at": utils.utcnow(),
+ "launched_at": utils.utcnow(),
+ "terminated_at": utils.utcnow(),
+ "availability_zone": "",
+ "display_name": "test_server",
+ "display_description": "",
+ "locked": False,
+ "metadata": [],
+ #"address": ,
+ #"floating_ips": [{"address":ip} for ip in public_addresses]}
+ "uuid": "deadbeef-feed-edee-beef-d0ea7beefedd"}
+
+ return instance
+
+
+class ServerActionsTest(test.TestCase):
+
+ def setUp(self):
+ self.maxDiff = None
+ super(ServerActionsTest, self).setUp()
+ self.flags(verbose=True)
+ self.stubs = stubout.StubOutForTesting()
+ fakes.FakeAuthManager.reset_fake_data()
+ fakes.FakeAuthDatabase.data = {}
+ fakes.stub_out_auth(self.stubs)
+ self.stubs.Set(nova.db.api, 'instance_get', return_server_by_id)
+ self.stubs.Set(nova.db.api, 'instance_update', instance_update)
+
+ self.webreq = common.webob_factory('/v1.0/servers')
+
+ def tearDown(self):
+ self.stubs.UnsetAll()
+
+ def test_server_change_password(self):
+ body = {'changePassword': {'adminPass': '1234pass'}}
+ req = webob.Request.blank('/v1.0/servers/1/action')
+ req.method = 'POST'
+ req.content_type = 'application/json'
+ req.body = json.dumps(body)
+ res = req.get_response(fakes.wsgi_app())
+ self.assertEqual(res.status_int, 501)
+
+ def test_server_change_password_xml(self):
+ req = webob.Request.blank('/v1.0/servers/1/action')
+ req.method = 'POST'
+ req.content_type = 'application/xml'
+ req.body = '<changePassword adminPass="1234pass">'
+# res = req.get_response(fakes.wsgi_app())
+# self.assertEqual(res.status_int, 501)
+
+ def test_server_reboot(self):
+ body = dict(server=dict(
+ name='server_test', imageId=2, flavorId=2, metadata={},
+ personality={}))
+ req = webob.Request.blank('/v1.0/servers/1/action')
+ req.method = 'POST'
+ req.content_type = 'application/json'
+ req.body = json.dumps(body)
+ res = req.get_response(fakes.wsgi_app())
+
+ def test_server_rebuild_accepted(self):
+ body = {
+ "rebuild": {
+ "imageId": 2,
+ },
+ }
+
+ req = webob.Request.blank('/v1.0/servers/1/action')
+ req.method = 'POST'
+ req.content_type = 'application/json'
+ req.body = json.dumps(body)
+
+ res = req.get_response(fakes.wsgi_app())
+ self.assertEqual(res.status_int, 202)
+ self.assertEqual(res.body, "")
+
+ def test_server_rebuild_rejected_when_building(self):
+ body = {
+ "rebuild": {
+ "imageId": 2,
+ },
+ }
+
+ state = power_state.BUILDING
+ new_return_server = return_server_with_power_state(state)
+ self.stubs.Set(nova.db.api, 'instance_get', new_return_server)
+ self.stubs.Set(nova.db, 'instance_get_by_uuid',
+ return_server_with_uuid_and_power_state(state))
+
+ req = webob.Request.blank('/v1.0/servers/1/action')
+ req.method = 'POST'
+ req.content_type = 'application/json'
+ req.body = json.dumps(body)
+
+ res = req.get_response(fakes.wsgi_app())
+ self.assertEqual(res.status_int, 409)
+
+ def test_server_rebuild_bad_entity(self):
+ body = {
+ "rebuild": {
+ },
+ }
+
+ req = webob.Request.blank('/v1.0/servers/1/action')
+ req.method = 'POST'
+ req.content_type = 'application/json'
+ req.body = json.dumps(body)
+
+ res = req.get_response(fakes.wsgi_app())
+ self.assertEqual(res.status_int, 400)
+
+ def test_resize_server(self):
+ req = self.webreq('/1/action', 'POST', dict(resize=dict(flavorId=3)))
+
+ self.resize_called = False
+
+ def resize_mock(*args):
+ self.resize_called = True
+
+ self.stubs.Set(nova.compute.api.API, 'resize', resize_mock)
+
+ res = req.get_response(fakes.wsgi_app())
+ self.assertEqual(res.status_int, 202)
+ self.assertEqual(self.resize_called, True)
+
+ def test_resize_bad_flavor_fails(self):
+ req = self.webreq('/1/action', 'POST', dict(resize=dict(derp=3)))
+
+ self.resize_called = False
+
+ def resize_mock(*args):
+ self.resize_called = True
+
+ self.stubs.Set(nova.compute.api.API, 'resize', resize_mock)
+
+ res = req.get_response(fakes.wsgi_app())
+ self.assertEqual(res.status_int, 400)
+ self.assertEqual(self.resize_called, False)
+
+ def test_resize_raises_fails(self):
+ req = self.webreq('/1/action', 'POST', dict(resize=dict(flavorId=3)))
+
+ def resize_mock(*args):
+ raise Exception('hurr durr')
+
+ self.stubs.Set(nova.compute.api.API, 'resize', resize_mock)
+
+ res = req.get_response(fakes.wsgi_app())
+ self.assertEqual(res.status_int, 500)
+
+ def test_resized_server_has_correct_status(self):
+ req = self.webreq('/1', 'GET')
+
+ def fake_migration_get(*args):
+ return {}
+
+ self.stubs.Set(nova.db, 'migration_get_by_instance_and_status',
+ fake_migration_get)
+ res = req.get_response(fakes.wsgi_app())
+ self.assertEqual(res.status_int, 200)
+ body = json.loads(res.body)
+ self.assertEqual(body['server']['status'], 'RESIZE-CONFIRM')
+
+ def test_confirm_resize_server(self):
+ req = self.webreq('/1/action', 'POST', dict(confirmResize=None))
+
+ self.resize_called = False
+
+ def confirm_resize_mock(*args):
+ self.resize_called = True
+
+ self.stubs.Set(nova.compute.api.API, 'confirm_resize',
+ confirm_resize_mock)
+
+ res = req.get_response(fakes.wsgi_app())
+ self.assertEqual(res.status_int, 204)
+ self.assertEqual(self.resize_called, True)
+
+ def test_confirm_resize_server_fails(self):
+ req = self.webreq('/1/action', 'POST', dict(confirmResize=None))
+
+ def confirm_resize_mock(*args):
+ raise Exception('hurr durr')
+
+ self.stubs.Set(nova.compute.api.API, 'confirm_resize',
+ confirm_resize_mock)
+
+ res = req.get_response(fakes.wsgi_app())
+ self.assertEqual(res.status_int, 400)
+
+ def test_revert_resize_server(self):
+ req = self.webreq('/1/action', 'POST', dict(revertResize=None))
+
+ self.resize_called = False
+
+ def revert_resize_mock(*args):
+ self.resize_called = True
+
+ self.stubs.Set(nova.compute.api.API, 'revert_resize',
+ revert_resize_mock)
+
+ res = req.get_response(fakes.wsgi_app())
+ self.assertEqual(res.status_int, 202)
+ self.assertEqual(self.resize_called, True)
+
+ def test_revert_resize_server_fails(self):
+ req = self.webreq('/1/action', 'POST', dict(revertResize=None))
+
+ def revert_resize_mock(*args):
+ raise Exception('hurr durr')
+
+ self.stubs.Set(nova.compute.api.API, 'revert_resize',
+ revert_resize_mock)
+
+ res = req.get_response(fakes.wsgi_app())
+ self.assertEqual(res.status_int, 400)
+
+ def test_migrate_server(self):
+ """This is basically the same as resize, only we provide the `migrate`
+ attribute in the body's dict.
+ """
+ req = self.webreq('/1/migrate', 'POST')
+
+ self.resize_called = False
+
+ def resize_mock(*args):
+ self.resize_called = True
+
+ self.stubs.Set(nova.compute.api.API, 'resize', resize_mock)
+
+ res = req.get_response(fakes.wsgi_app())
+ self.assertEqual(res.status_int, 202)
+ self.assertEqual(self.resize_called, True)
+
+ def test_create_backup(self):
+ """The happy path for creating backups"""
+ self.flags(allow_admin_api=True)
+
+ body = {
+ 'createBackup': {
+ 'name': 'Backup 1',
+ 'backup_type': 'daily',
+ 'rotation': 1,
+ },
+ }
+
+ req = webob.Request.blank('/v1.0/servers/1/action')
+ req.method = 'POST'
+ req.body = json.dumps(body)
+ req.headers["content-type"] = "application/json"
+ response = req.get_response(fakes.wsgi_app())
+ self.assertEqual(202, response.status_int)
+ self.assertTrue(response.headers['Location'])
+
+ def test_create_backup_admin_api_off(self):
+ """The happy path for creating backups"""
+ self.flags(allow_admin_api=False)
+
+ body = {
+ 'createBackup': {
+ 'name': 'Backup 1',
+ 'backup_type': 'daily',
+ 'rotation': 1,
+ },
+ }
+
+ req = webob.Request.blank('/v1.0/servers/1/action')
+ req.method = 'POST'
+ req.body = json.dumps(body)
+ req.headers["content-type"] = "application/json"
+ response = req.get_response(fakes.wsgi_app())
+ self.assertEqual(501, response.status_int)
+
+ def test_create_backup_with_metadata(self):
+ self.flags(allow_admin_api=True)
+
+ body = {
+ 'createBackup': {
+ 'name': 'Backup 1',
+ 'backup_type': 'daily',
+ 'rotation': 1,
+ 'metadata': {'123': 'asdf'},
+ },
+ }
+
+ req = webob.Request.blank('/v1.0/servers/1/action')
+ req.method = 'POST'
+ req.body = json.dumps(body)
+ req.headers["content-type"] = "application/json"
+ response = req.get_response(fakes.wsgi_app())
+ self.assertEqual(202, response.status_int)
+ self.assertTrue(response.headers['Location'])
+
+ def test_create_backup_with_too_much_metadata(self):
+ self.flags(allow_admin_api=True)
+
+ body = {
+ 'createBackup': {
+ 'name': 'Backup 1',
+ 'backup_type': 'daily',
+ 'rotation': 1,
+ 'metadata': {'123': 'asdf'},
+ },
+ }
+ for num in range(FLAGS.quota_metadata_items + 1):
+ body['createBackup']['metadata']['foo%i' % num] = "bar"
+ req = webob.Request.blank('/v1.0/servers/1/action')
+ req.method = 'POST'
+ req.body = json.dumps(body)
+ req.headers["content-type"] = "application/json"
+ response = req.get_response(fakes.wsgi_app())
+ self.assertEqual(400, response.status_int)
+
+ def test_create_backup_no_name(self):
+ """Name is required for backups"""
+ self.flags(allow_admin_api=True)
+
+ body = {
+ 'createBackup': {
+ 'backup_type': 'daily',
+ 'rotation': 1,
+ },
+ }
+
+ req = webob.Request.blank('/v1.0/images')
+ req.method = 'POST'
+ req.body = json.dumps(body)
+ req.headers["content-type"] = "application/json"
+ response = req.get_response(fakes.wsgi_app())
+ self.assertEqual(400, response.status_int)
+
+ def test_create_backup_no_rotation(self):
+ """Rotation is required for backup requests"""
+ self.flags(allow_admin_api=True)
+
+ body = {
+ 'createBackup': {
+ 'name': 'Backup 1',
+ 'backup_type': 'daily',
+ },
+ }
+
+ req = webob.Request.blank('/v1.0/images')
+ req.method = 'POST'
+ req.body = json.dumps(body)
+ req.headers["content-type"] = "application/json"
+
+ response = req.get_response(fakes.wsgi_app())
+ self.assertEqual(400, response.status_int)
+
+ def test_create_backup_no_backup_type(self):
+ """Backup Type (daily or weekly) is required for backup requests"""
+ self.flags(allow_admin_api=True)
+
+ body = {
+ 'createBackup': {
+ 'name': 'Backup 1',
+ 'rotation': 1,
+ },
+ }
+ req = webob.Request.blank('/v1.0/images')
+ req.method = 'POST'
+ req.body = json.dumps(body)
+ req.headers["content-type"] = "application/json"
+
+ response = req.get_response(fakes.wsgi_app())
+ self.assertEqual(400, response.status_int)
+
+ def test_create_backup_bad_entity(self):
+ self.flags(allow_admin_api=True)
+
+ body = {'createBackup': 'go'}
+ req = webob.Request.blank('/v1.0/images')
+ req.method = 'POST'
+ req.body = json.dumps(body)
+ req.headers["content-type"] = "application/json"
+
+ response = req.get_response(fakes.wsgi_app())
+ self.assertEqual(400, response.status_int)
+
+
+class ServerActionsTestV11(test.TestCase):
+
+ def setUp(self):
+ self.maxDiff = None
+ super(ServerActionsTestV11, self).setUp()
+ self.stubs = stubout.StubOutForTesting()
+ fakes.FakeAuthManager.reset_fake_data()
+ fakes.FakeAuthDatabase.data = {}
+ fakes.stub_out_auth(self.stubs)
+ self.stubs.Set(nova.db.api, 'instance_get', return_server_by_id)
+ self.stubs.Set(nova.db.api, 'instance_update', instance_update)
+
+ fakes.stub_out_glance(self.stubs)
+ fakes.stub_out_compute_api_snapshot(self.stubs)
+ service_class = 'nova.image.glance.GlanceImageService'
+ self.service = utils.import_object(service_class)
+ self.context = context.RequestContext(1, None)
+ self.service.delete_all()
+ self.sent_to_glance = {}
+ fakes.stub_out_glance_add_image(self.stubs, self.sent_to_glance)
+ self.flags(allow_instance_snapshots=True)
+
+ def tearDown(self):
+ self.stubs.UnsetAll()
+
+ def test_server_change_password(self):
+ mock_method = MockSetAdminPassword()
+ self.stubs.Set(nova.compute.api.API, 'set_admin_password', mock_method)
+ body = {'changePassword': {'adminPass': '1234pass'}}
+ req = webob.Request.blank('/v1.1/servers/1/action')
+ req.method = 'POST'
+ req.content_type = 'application/json'
+ req.body = json.dumps(body)
+ res = req.get_response(fakes.wsgi_app())
+ self.assertEqual(res.status_int, 202)
+ self.assertEqual(mock_method.instance_id, '1')
+ self.assertEqual(mock_method.password, '1234pass')
+
+ def test_server_change_password_xml(self):
+ mock_method = MockSetAdminPassword()
+ self.stubs.Set(nova.compute.api.API, 'set_admin_password', mock_method)
+ req = webob.Request.blank('/v1.1/servers/1/action')
+ req.method = 'POST'
+ req.content_type = "application/xml"
+ req.body = """<?xml version="1.0" encoding="UTF-8"?>
+ <changePassword
+ xmlns="http://docs.openstack.org/compute/api/v1.1"
+ adminPass="1234pass"/>"""
+ res = req.get_response(fakes.wsgi_app())
+ self.assertEqual(res.status_int, 202)
+ self.assertEqual(mock_method.instance_id, '1')
+ self.assertEqual(mock_method.password, '1234pass')
+
+ def test_server_change_password_not_a_string(self):
+ body = {'changePassword': {'adminPass': 1234}}
+ req = webob.Request.blank('/v1.1/servers/1/action')
+ req.method = 'POST'
+ req.content_type = 'application/json'
+ req.body = json.dumps(body)
+ res = req.get_response(fakes.wsgi_app())
+ self.assertEqual(res.status_int, 400)
+
+ def test_server_change_password_bad_request(self):
+ body = {'changePassword': {'pass': '12345'}}
+ req = webob.Request.blank('/v1.1/servers/1/action')
+ req.method = 'POST'
+ req.content_type = 'application/json'
+ req.body = json.dumps(body)
+ res = req.get_response(fakes.wsgi_app())
+ self.assertEqual(res.status_int, 400)
+
+ def test_server_change_password_empty_string(self):
+ body = {'changePassword': {'adminPass': ''}}
+ req = webob.Request.blank('/v1.1/servers/1/action')
+ req.method = 'POST'
+ req.content_type = 'application/json'
+ req.body = json.dumps(body)
+ res = req.get_response(fakes.wsgi_app())
+ self.assertEqual(res.status_int, 400)
+
+ def test_server_change_password_none(self):
+ body = {'changePassword': {'adminPass': None}}
+ req = webob.Request.blank('/v1.1/servers/1/action')
+ req.method = 'POST'
+ req.content_type = 'application/json'
+ req.body = json.dumps(body)
+ res = req.get_response(fakes.wsgi_app())
+ self.assertEqual(res.status_int, 400)
+
+ def test_server_reboot_hard(self):
+ body = dict(reboot=dict(type="HARD"))
+ req = webob.Request.blank('/v1.1/servers/1/action')
+ req.method = 'POST'
+ req.content_type = 'application/json'
+ req.body = json.dumps(body)
+ res = req.get_response(fakes.wsgi_app())
+ self.assertEqual(res.status_int, 202)
+
+ def test_server_reboot_soft(self):
+ body = dict(reboot=dict(type="SOFT"))
+ req = webob.Request.blank('/v1.1/servers/1/action')
+ req.method = 'POST'
+ req.content_type = 'application/json'
+ req.body = json.dumps(body)
+ res = req.get_response(fakes.wsgi_app())
+ self.assertEqual(res.status_int, 202)
+
+ def test_server_reboot_incorrect_type(self):
+ body = dict(reboot=dict(type="NOT_A_TYPE"))
+ req = webob.Request.blank('/v1.1/servers/1/action')
+ req.method = 'POST'
+ req.content_type = 'application/json'
+ req.body = json.dumps(body)
+ res = req.get_response(fakes.wsgi_app())
+ self.assertEqual(res.status_int, 400)
+
+ def test_server_reboot_missing_type(self):
+ body = dict(reboot=dict())
+ req = webob.Request.blank('/v1.1/servers/1/action')
+ req.method = 'POST'
+ req.content_type = 'application/json'
+ req.body = json.dumps(body)
+ res = req.get_response(fakes.wsgi_app())
+ self.assertEqual(res.status_int, 400)
+
+ def test_server_rebuild_accepted_minimum(self):
+ body = {
+ "rebuild": {
+ "imageRef": "http://localhost/images/2",
+ },
+ }
+
+ req = webob.Request.blank('/v1.1/servers/1/action')
+ req.method = 'POST'
+ req.content_type = 'application/json'
+ req.body = json.dumps(body)
+
+ res = req.get_response(fakes.wsgi_app())
+ self.assertEqual(res.status_int, 202)
+
+ def test_server_rebuild_rejected_when_building(self):
+ body = {
+ "rebuild": {
+ "imageRef": "http://localhost/images/2",
+ },
+ }
+
+ state = power_state.BUILDING
+ new_return_server = return_server_with_power_state(state)
+ self.stubs.Set(nova.db.api, 'instance_get', new_return_server)
+ self.stubs.Set(nova.db, 'instance_get_by_uuid',
+ return_server_with_uuid_and_power_state(state))
+
+ req = webob.Request.blank('/v1.1/servers/1/action')
+ req.method = 'POST'
+ req.content_type = 'application/json'
+ req.body = json.dumps(body)
+
+ res = req.get_response(fakes.wsgi_app())
+ self.assertEqual(res.status_int, 409)
+
+ def test_server_rebuild_accepted_with_metadata(self):
+ body = {
+ "rebuild": {
+ "imageRef": "http://localhost/images/2",
+ "metadata": {
+ "new": "metadata",
+ },
+ },
+ }
+
+ req = webob.Request.blank('/v1.1/servers/1/action')
+ req.method = 'POST'
+ req.content_type = 'application/json'
+ req.body = json.dumps(body)
+
+ res = req.get_response(fakes.wsgi_app())
+ self.assertEqual(res.status_int, 202)
+
+ def test_server_rebuild_accepted_with_bad_metadata(self):
+ body = {
+ "rebuild": {
+ "imageRef": "http://localhost/images/2",
+ "metadata": "stack",
+ },
+ }
+
+ req = webob.Request.blank('/v1.1/servers/1/action')
+ req.method = 'POST'
+ req.content_type = 'application/json'
+ req.body = json.dumps(body)
+
+ res = req.get_response(fakes.wsgi_app())
+ self.assertEqual(res.status_int, 400)
+
+ def test_server_rebuild_bad_entity(self):
+ body = {
+ "rebuild": {
+ "imageId": 2,
+ },
+ }
+
+ req = webob.Request.blank('/v1.1/servers/1/action')
+ req.method = 'POST'
+ req.content_type = 'application/json'
+ req.body = json.dumps(body)
+
+ res = req.get_response(fakes.wsgi_app())
+ self.assertEqual(res.status_int, 400)
+
+ def test_server_rebuild_bad_personality(self):
+ body = {
+ "rebuild": {
+ "imageRef": "http://localhost/images/2",
+ "personality": [{
+ "path": "/path/to/file",
+ "contents": "INVALID b64",
+ }]
+ },
+ }
+
+ req = webob.Request.blank('/v1.1/servers/1/action')
+ req.method = 'POST'
+ req.content_type = 'application/json'
+ req.body = json.dumps(body)
+
+ res = req.get_response(fakes.wsgi_app())
+ self.assertEqual(res.status_int, 400)
+
+ def test_server_rebuild_personality(self):
+ body = {
+ "rebuild": {
+ "imageRef": "http://localhost/images/2",
+ "personality": [{
+ "path": "/path/to/file",
+ "contents": base64.b64encode("Test String"),
+ }]
+ },
+ }
+
+ req = webob.Request.blank('/v1.1/servers/1/action')
+ req.method = 'POST'
+ req.content_type = 'application/json'
+ req.body = json.dumps(body)
+
+ res = req.get_response(fakes.wsgi_app())
+ self.assertEqual(res.status_int, 202)
+
+ def test_resize_server(self):
+
+ req = webob.Request.blank('/v1.1/servers/1/action')
+ req.content_type = 'application/json'
+ req.method = 'POST'
+ body_dict = dict(resize=dict(flavorRef="http://localhost/3"))
+ req.body = json.dumps(body_dict)
+
+ self.resize_called = False
+
+ def resize_mock(*args):
+ self.resize_called = True
+
+ self.stubs.Set(nova.compute.api.API, 'resize', resize_mock)
+
+ res = req.get_response(fakes.wsgi_app())
+ self.assertEqual(res.status_int, 202)
+ self.assertEqual(self.resize_called, True)
+
+ def test_resize_server_no_flavor(self):
+ req = webob.Request.blank('/v1.1/servers/1/action')
+ req.content_type = 'application/json'
+ req.method = 'POST'
+ body_dict = dict(resize=dict())
+ req.body = json.dumps(body_dict)
+
+ res = req.get_response(fakes.wsgi_app())
+ self.assertEqual(res.status_int, 400)
+
+ def test_resize_server_no_flavor_ref(self):
+ req = webob.Request.blank('/v1.1/servers/1/action')
+ req.content_type = 'application/json'
+ req.method = 'POST'
+ body_dict = dict(resize=dict(flavorRef=None))
+ req.body = json.dumps(body_dict)
+
+ res = req.get_response(fakes.wsgi_app())
+ self.assertEqual(res.status_int, 400)
+
+ def test_confirm_resize_server(self):
+ req = webob.Request.blank('/v1.1/servers/1/action')
+ req.content_type = 'application/json'
+ req.method = 'POST'
+ body_dict = dict(confirmResize=None)
+ req.body = json.dumps(body_dict)
+
+ self.confirm_resize_called = False
+
+ def cr_mock(*args):
+ self.confirm_resize_called = True
+
+ self.stubs.Set(nova.compute.api.API, 'confirm_resize', cr_mock)
+
+ res = req.get_response(fakes.wsgi_app())
+ self.assertEqual(res.status_int, 204)
+ self.assertEqual(self.confirm_resize_called, True)
+
+ def test_revert_resize_server(self):
+ req = webob.Request.blank('/v1.1/servers/1/action')
+ req.content_type = 'application/json'
+ req.method = 'POST'
+ body_dict = dict(revertResize=None)
+ req.body = json.dumps(body_dict)
+
+ self.revert_resize_called = False
+
+ def revert_mock(*args):
+ self.revert_resize_called = True
+
+ self.stubs.Set(nova.compute.api.API, 'revert_resize', revert_mock)
+
+ res = req.get_response(fakes.wsgi_app())
+ self.assertEqual(res.status_int, 202)
+ self.assertEqual(self.revert_resize_called, True)
+
+ def test_create_image(self):
+ body = {
+ 'createImage': {
+ 'name': 'Snapshot 1',
+ },
+ }
+ req = webob.Request.blank('/v1.1/servers/1/action')
+ req.method = 'POST'
+ req.body = json.dumps(body)
+ req.headers["content-type"] = "application/json"
+ response = req.get_response(fakes.wsgi_app())
+ self.assertEqual(202, response.status_int)
+ location = response.headers['Location']
+ self.assertEqual('http://localhost/v1.1/images/123', location)
+
+ def test_create_image_snapshots_disabled(self):
+ """Don't permit a snapshot if the allow_instance_snapshots flag is
+ False
+ """
+ self.flags(allow_instance_snapshots=False)
+ body = {
+ 'createImage': {
+ 'name': 'Snapshot 1',
+ },
+ }
+ req = webob.Request.blank('/v1.1/servers/1/action')
+ req.method = 'POST'
+ req.body = json.dumps(body)
+ req.headers["content-type"] = "application/json"
+ response = req.get_response(fakes.wsgi_app())
+ self.assertEqual(400, response.status_int)
+
+ def test_create_image_with_metadata(self):
+ body = {
+ 'createImage': {
+ 'name': 'Snapshot 1',
+ 'metadata': {'key': 'asdf'},
+ },
+ }
+ req = webob.Request.blank('/v1.1/servers/1/action')
+ req.method = 'POST'
+ req.body = json.dumps(body)
+ req.headers["content-type"] = "application/json"
+ response = req.get_response(fakes.wsgi_app())
+ self.assertEqual(202, response.status_int)
+ location = response.headers['Location']
+ self.assertEqual('http://localhost/v1.1/images/123', location)
+
+ def test_create_image_with_too_much_metadata(self):
+ body = {
+ 'createImage': {
+ 'name': 'Snapshot 1',
+ 'metadata': {},
+ },
+ }
+ for num in range(FLAGS.quota_metadata_items + 1):
+ body['createImage']['metadata']['foo%i' % num] = "bar"
+ req = webob.Request.blank('/v1.1/servers/1/action')
+ req.method = 'POST'
+ req.body = json.dumps(body)
+ req.headers["content-type"] = "application/json"
+ response = req.get_response(fakes.wsgi_app())
+ self.assertEqual(400, response.status_int)
+
+ def test_create_image_no_name(self):
+ body = {
+ 'createImage': {},
+ }
+ req = webob.Request.blank('/v1.1/servers/1/action')
+ req.method = 'POST'
+ req.body = json.dumps(body)
+ req.headers["content-type"] = "application/json"
+ response = req.get_response(fakes.wsgi_app())
+ self.assertEqual(400, response.status_int)
+
+ def test_create_image_bad_metadata(self):
+ body = {
+ 'createImage': {
+ 'name': 'geoff',
+ 'metadata': 'henry',
+ },
+ }
+ req = webob.Request.blank('/v1.1/servers/1/action')
+ req.method = 'POST'
+ req.body = json.dumps(body)
+ req.headers["content-type"] = "application/json"
+ response = req.get_response(fakes.wsgi_app())
+ self.assertEqual(400, response.status_int)
+
+ def test_create_backup(self):
+ """The happy path for creating backups"""
+ self.flags(allow_admin_api=True)
+
+ body = {
+ 'createBackup': {
+ 'name': 'Backup 1',
+ 'backup_type': 'daily',
+ 'rotation': 1,
+ },
+ }
+
+ req = webob.Request.blank('/v1.1/servers/1/action')
+ req.method = 'POST'
+ req.body = json.dumps(body)
+ req.headers["content-type"] = "application/json"
+ response = req.get_response(fakes.wsgi_app())
+ self.assertEqual(202, response.status_int)
+ self.assertTrue(response.headers['Location'])
+
+
+class TestServerActionXMLDeserializerV11(test.TestCase):
+
+ def setUp(self):
+ self.deserializer = create_instance_helper.ServerXMLDeserializerV11()
+
+ def tearDown(self):
+ pass
+
+ def test_create_image(self):
+ serial_request = """
+<createImage xmlns="http://docs.openstack.org/compute/api/v1.1"
+ name="new-server-test"/>"""
+ request = self.deserializer.deserialize(serial_request, 'action')
+ expected = {
+ "createImage": {
+ "name": "new-server-test",
+ },
+ }
+ self.assertEquals(request['body'], expected)
+
+ def test_create_image_with_metadata(self):
+ serial_request = """
+<createImage xmlns="http://docs.openstack.org/compute/api/v1.1"
+ name="new-server-test">
+ <metadata>
+ <meta key="key1">value1</meta>
+ </metadata>
+</createImage>"""
+ request = self.deserializer.deserialize(serial_request, 'action')
+ expected = {
+ "createImage": {
+ "name": "new-server-test",
+ "metadata": {"key1": "value1"},
+ },
+ }
+ self.assertEquals(request['body'], expected)
+
+ def test_change_pass(self):
+ serial_request = """<?xml version="1.0" encoding="UTF-8"?>
+ <changePassword
+ xmlns="http://docs.openstack.org/compute/api/v1.1"
+ adminPass="1234pass"/> """
+ request = self.deserializer.deserialize(serial_request, 'action')
+ expected = {
+ "changePassword": {
+ "adminPass": "1234pass",
+ },
+ }
+ self.assertEquals(request['body'], expected)
+
+ def test_change_pass_no_pass(self):
+ serial_request = """<?xml version="1.0" encoding="UTF-8"?>
+ <changePassword
+ xmlns="http://docs.openstack.org/compute/api/v1.1"/> """
+ self.assertRaises(AttributeError,
+ self.deserializer.deserialize,
+ serial_request,
+ 'action')
+
+ def test_reboot(self):
+ serial_request = """<?xml version="1.0" encoding="UTF-8"?>
+ <reboot
+ xmlns="http://docs.openstack.org/compute/api/v1.1"
+ type="HARD"/>"""
+ request = self.deserializer.deserialize(serial_request, 'action')
+ expected = {
+ "reboot": {
+ "type": "HARD",
+ },
+ }
+ self.assertEquals(request['body'], expected)
+
+ def test_reboot_no_type(self):
+ serial_request = """<?xml version="1.0" encoding="UTF-8"?>
+ <reboot
+ xmlns="http://docs.openstack.org/compute/api/v1.1"/>"""
+ self.assertRaises(AttributeError,
+ self.deserializer.deserialize,
+ serial_request,
+ 'action')
+
+ def test_resize(self):
+ serial_request = """<?xml version="1.0" encoding="UTF-8"?>
+ <resize
+ xmlns="http://docs.openstack.org/compute/api/v1.1"
+ flavorRef="http://localhost/flavors/3"/>"""
+ request = self.deserializer.deserialize(serial_request, 'action')
+ expected = {
+ "resize": {"flavorRef": "http://localhost/flavors/3"},
+ }
+ self.assertEquals(request['body'], expected)
+
+ def test_resize_no_flavor_ref(self):
+ serial_request = """<?xml version="1.0" encoding="UTF-8"?>
+ <resize
+ xmlns="http://docs.openstack.org/compute/api/v1.1"/>"""
+ self.assertRaises(AttributeError,
+ self.deserializer.deserialize,
+ serial_request,
+ 'action')
+
+ def test_confirm_resize(self):
+ serial_request = """<?xml version="1.0" encoding="UTF-8"?>
+ <confirmResize
+ xmlns="http://docs.openstack.org/compute/api/v1.1"/>"""
+ request = self.deserializer.deserialize(serial_request, 'action')
+ expected = {
+ "confirmResize": None,
+ }
+ self.assertEquals(request['body'], expected)
+
+ def test_revert_resize(self):
+ serial_request = """<?xml version="1.0" encoding="UTF-8"?>
+ <revertResize
+ xmlns="http://docs.openstack.org/compute/api/v1.1"/>"""
+ request = self.deserializer.deserialize(serial_request, 'action')
+ expected = {
+ "revertResize": None,
+ }
+ self.assertEquals(request['body'], expected)
+
+ def test_rebuild(self):
+ serial_request = """<?xml version="1.0" encoding="UTF-8"?>
+ <rebuild
+ xmlns="http://docs.openstack.org/compute/api/v1.1"
+ name="new-server-test"
+ imageRef="http://localhost/images/1">
+ <metadata>
+ <meta key="My Server Name">Apache1</meta>
+ </metadata>
+ <personality>
+ <file path="/etc/banner.txt">Mg==</file>
+ </personality>
+ </rebuild>"""
+ request = self.deserializer.deserialize(serial_request, 'action')
+ expected = {
+ "rebuild": {
+ "name": "new-server-test",
+ "imageRef": "http://localhost/images/1",
+ "metadata": {
+ "My Server Name": "Apache1",
+ },
+ "personality": [
+ {"path": "/etc/banner.txt", "contents": "Mg=="},
+ ],
+ },
+ }
+ self.assertDictMatch(request['body'], expected)
+
+ def test_rebuild_minimum(self):
+ serial_request = """<?xml version="1.0" encoding="UTF-8"?>
+ <rebuild
+ xmlns="http://docs.openstack.org/compute/api/v1.1"
+ imageRef="http://localhost/images/1"/>"""
+ request = self.deserializer.deserialize(serial_request, 'action')
+ expected = {
+ "rebuild": {
+ "imageRef": "http://localhost/images/1",
+ },
+ }
+ self.assertDictMatch(request['body'], expected)
+
+ def test_rebuild_no_imageRef(self):
+ serial_request = """<?xml version="1.0" encoding="UTF-8"?>
+ <rebuild
+ xmlns="http://docs.openstack.org/compute/api/v1.1"
+ name="new-server-test">
+ <metadata>
+ <meta key="My Server Name">Apache1</meta>
+ </metadata>
+ <personality>
+ <file path="/etc/banner.txt">Mg==</file>
+ </personality>
+ </rebuild>"""
+ self.assertRaises(AttributeError,
+ self.deserializer.deserialize,
+ serial_request,
+ 'action')
diff --git a/nova/tests/api/openstack/test_server_metadata.py b/nova/tests/api/openstack/test_server_metadata.py
index f90485067..ec446f0f0 100644
--- a/nova/tests/api/openstack/test_server_metadata.py
+++ b/nova/tests/api/openstack/test_server_metadata.py
@@ -17,7 +17,7 @@
import json
import webob
-
+from xml.dom import minidom
from nova import exception
from nova import flags
@@ -29,11 +29,11 @@ import nova.wsgi
FLAGS = flags.FLAGS
-def return_create_instance_metadata_max(context, server_id, metadata):
+def return_create_instance_metadata_max(context, server_id, metadata, delete):
return stub_max_server_metadata()
-def return_create_instance_metadata(context, server_id, metadata):
+def return_create_instance_metadata(context, server_id, metadata, delete):
return stub_server_metadata()
@@ -51,11 +51,10 @@ def delete_server_metadata(context, server_id, key):
def stub_server_metadata():
metadata = {
- "key1": "value1",
- "key2": "value2",
- "key3": "value3",
- "key4": "value4",
- "key5": "value5"}
+ "key1": "value1",
+ "key2": "value2",
+ "key3": "value3",
+ }
return metadata
@@ -84,94 +83,182 @@ class ServerMetaDataTest(test.TestCase):
def test_index(self):
self.stubs.Set(nova.db.api, 'instance_metadata_get',
return_server_metadata)
- req = webob.Request.blank('/v1.1/servers/1/meta')
- req.environ['api.version'] = '1.1'
+ req = webob.Request.blank('/v1.1/servers/1/metadata')
res = req.get_response(fakes.wsgi_app())
- res_dict = json.loads(res.body)
self.assertEqual(200, res.status_int)
+ res_dict = json.loads(res.body)
self.assertEqual('application/json', res.headers['Content-Type'])
- self.assertEqual('value1', res_dict['metadata']['key1'])
+ expected = {
+ 'metadata': {
+ 'key1': 'value1',
+ 'key2': 'value2',
+ 'key3': 'value3',
+ },
+ }
+ self.assertEqual(expected, res_dict)
+
+ def test_index_xml(self):
+ self.stubs.Set(nova.db.api, 'instance_metadata_get',
+ return_server_metadata)
+ request = webob.Request.blank("/v1.1/servers/1/metadata")
+ request.accept = "application/xml"
+ response = request.get_response(fakes.wsgi_app())
+ self.assertEqual(200, response.status_int)
+ self.assertEqual("application/xml", response.content_type)
+
+ actual_metadata = minidom.parseString(response.body.replace(" ", ""))
+
+ expected_metadata = minidom.parseString("""
+ <metadata xmlns="http://docs.openstack.org/compute/api/v1.1">
+ <meta key="key3">value3</meta>
+ <meta key="key2">value2</meta>
+ <meta key="key1">value1</meta>
+ </metadata>
+ """.replace(" ", "").replace("\n", ""))
+
+ self.assertEqual(expected_metadata.toxml(), actual_metadata.toxml())
def test_index_nonexistant_server(self):
self.stubs.Set(nova.db.api, 'instance_get', return_server_nonexistant)
- req = webob.Request.blank('/v1.1/servers/1/meta')
- req.environ['api.version'] = '1.1'
+ req = webob.Request.blank('/v1.1/servers/1/metadata')
res = req.get_response(fakes.wsgi_app())
self.assertEqual(404, res.status_int)
def test_index_no_data(self):
self.stubs.Set(nova.db.api, 'instance_metadata_get',
return_empty_server_metadata)
- req = webob.Request.blank('/v1.1/servers/1/meta')
- req.environ['api.version'] = '1.1'
+ req = webob.Request.blank('/v1.1/servers/1/metadata')
res = req.get_response(fakes.wsgi_app())
- res_dict = json.loads(res.body)
self.assertEqual(200, res.status_int)
- self.assertEqual('application/json', res.headers['Content-Type'])
- self.assertEqual(0, len(res_dict['metadata']))
+ res_dict = json.loads(res.body)
+ expected = {'metadata': {}}
+ self.assertEqual(expected, res_dict)
def test_show(self):
self.stubs.Set(nova.db.api, 'instance_metadata_get',
return_server_metadata)
- req = webob.Request.blank('/v1.1/servers/1/meta/key5')
- req.environ['api.version'] = '1.1'
+ req = webob.Request.blank('/v1.1/servers/1/metadata/key2')
res = req.get_response(fakes.wsgi_app())
res_dict = json.loads(res.body)
self.assertEqual(200, res.status_int)
- self.assertEqual('application/json', res.headers['Content-Type'])
- self.assertEqual('value5', res_dict['key5'])
+ expected = {'meta': {'key2': 'value2'}}
+ self.assertEqual(expected, res_dict)
+
+ def test_show_xml(self):
+ self.stubs.Set(nova.db.api, 'instance_metadata_get',
+ return_server_metadata)
+ request = webob.Request.blank("/v1.1/servers/1/metadata/key2")
+ request.accept = "application/xml"
+ response = request.get_response(fakes.wsgi_app())
+ self.assertEqual(200, response.status_int)
+ self.assertEqual("application/xml", response.content_type)
+
+ actual_metadata = minidom.parseString(response.body.replace(" ", ""))
+
+ expected_metadata = minidom.parseString("""
+ <meta xmlns="http://docs.openstack.org/compute/api/v1.1"
+ key="key2">value2</meta>
+ """.replace(" ", "").replace("\n", ""))
+
+ self.assertEqual(expected_metadata.toxml(), actual_metadata.toxml())
def test_show_nonexistant_server(self):
self.stubs.Set(nova.db.api, 'instance_get', return_server_nonexistant)
- req = webob.Request.blank('/v1.1/servers/1/meta/key5')
- req.environ['api.version'] = '1.1'
+ req = webob.Request.blank('/v1.1/servers/1/metadata/key2')
res = req.get_response(fakes.wsgi_app())
self.assertEqual(404, res.status_int)
def test_show_meta_not_found(self):
self.stubs.Set(nova.db.api, 'instance_metadata_get',
return_empty_server_metadata)
- req = webob.Request.blank('/v1.1/servers/1/meta/key6')
- req.environ['api.version'] = '1.1'
+ req = webob.Request.blank('/v1.1/servers/1/metadata/key6')
res = req.get_response(fakes.wsgi_app())
self.assertEqual(404, res.status_int)
def test_delete(self):
+ self.stubs.Set(nova.db.api, 'instance_metadata_get',
+ return_server_metadata)
self.stubs.Set(nova.db.api, 'instance_metadata_delete',
delete_server_metadata)
- req = webob.Request.blank('/v1.1/servers/1/meta/key5')
- req.environ['api.version'] = '1.1'
+ req = webob.Request.blank('/v1.1/servers/1/metadata/key2')
req.method = 'DELETE'
res = req.get_response(fakes.wsgi_app())
- self.assertEqual(200, res.status_int)
+ self.assertEqual(204, res.status_int)
+ self.assertEqual('', res.body)
def test_delete_nonexistant_server(self):
self.stubs.Set(nova.db.api, 'instance_get', return_server_nonexistant)
- req = webob.Request.blank('/v1.1/servers/1/meta/key5')
- req.environ['api.version'] = '1.1'
+ req = webob.Request.blank('/v1.1/servers/1/metadata/key1')
+ req.method = 'DELETE'
+ res = req.get_response(fakes.wsgi_app())
+ self.assertEqual(404, res.status_int)
+
+ def test_delete_meta_not_found(self):
+ self.stubs.Set(nova.db.api, 'instance_metadata_get',
+ return_empty_server_metadata)
+ req = webob.Request.blank('/v1.1/servers/1/metadata/key6')
req.method = 'DELETE'
res = req.get_response(fakes.wsgi_app())
self.assertEqual(404, res.status_int)
def test_create(self):
- self.stubs.Set(nova.db.api, 'instance_metadata_update_or_create',
+ self.stubs.Set(nova.db.api, 'instance_metadata_get',
+ return_server_metadata)
+ self.stubs.Set(nova.db.api, 'instance_metadata_update',
return_create_instance_metadata)
- req = webob.Request.blank('/v1.1/servers/1/meta')
- req.environ['api.version'] = '1.1'
+ req = webob.Request.blank('/v1.1/servers/1/metadata')
req.method = 'POST'
- req.body = '{"metadata": {"key1": "value1"}}'
- req.headers["content-type"] = "application/json"
+ req.content_type = "application/json"
+ input = {"metadata": {"key9": "value9"}}
+ req.body = json.dumps(input)
res = req.get_response(fakes.wsgi_app())
+
self.assertEqual(200, res.status_int)
res_dict = json.loads(res.body)
- self.assertEqual('application/json', res.headers['Content-Type'])
- self.assertEqual('value1', res_dict['metadata']['key1'])
+ input['metadata'].update({
+ "key1": "value1",
+ "key2": "value2",
+ "key3": "value3",
+ })
+ self.assertEqual(input, res_dict)
+
+ def test_create_xml(self):
+ self.stubs.Set(nova.db.api, 'instance_metadata_get',
+ return_server_metadata)
+ self.stubs.Set(nova.db.api, "instance_metadata_update",
+ return_create_instance_metadata)
+ req = webob.Request.blank("/v1.1/servers/1/metadata")
+ req.method = "POST"
+ req.content_type = "application/xml"
+ req.accept = "application/xml"
+
+ request_metadata = minidom.parseString("""
+ <metadata xmlns="http://docs.openstack.org/compute/api/v1.1">
+ <meta key="key5">value5</meta>
+ </metadata>
+ """.replace(" ", "").replace("\n", ""))
+
+ req.body = str(request_metadata.toxml())
+ response = req.get_response(fakes.wsgi_app())
+
+ expected_metadata = minidom.parseString("""
+ <metadata xmlns="http://docs.openstack.org/compute/api/v1.1">
+ <meta key="key3">value3</meta>
+ <meta key="key2">value2</meta>
+ <meta key="key1">value1</meta>
+ <meta key="key5">value5</meta>
+ </metadata>
+ """.replace(" ", "").replace("\n", ""))
+
+ self.assertEqual(200, response.status_int)
+ actual_metadata = minidom.parseString(response.body)
+
+ self.assertEqual(expected_metadata.toxml(), actual_metadata.toxml())
def test_create_empty_body(self):
- self.stubs.Set(nova.db.api, 'instance_metadata_update_or_create',
+ self.stubs.Set(nova.db.api, 'instance_metadata_update',
return_create_instance_metadata)
- req = webob.Request.blank('/v1.1/servers/1/meta')
- req.environ['api.version'] = '1.1'
+ req = webob.Request.blank('/v1.1/servers/1/metadata')
req.method = 'POST'
req.headers["content-type"] = "application/json"
res = req.get_response(fakes.wsgi_app())
@@ -179,79 +266,153 @@ class ServerMetaDataTest(test.TestCase):
def test_create_nonexistant_server(self):
self.stubs.Set(nova.db.api, 'instance_get', return_server_nonexistant)
- req = webob.Request.blank('/v1.1/servers/100/meta')
- req.environ['api.version'] = '1.1'
+ req = webob.Request.blank('/v1.1/servers/100/metadata')
req.method = 'POST'
req.body = '{"metadata": {"key1": "value1"}}'
req.headers["content-type"] = "application/json"
res = req.get_response(fakes.wsgi_app())
self.assertEqual(404, res.status_int)
+ def test_update_all(self):
+ self.stubs.Set(nova.db.api, 'instance_metadata_update',
+ return_create_instance_metadata)
+ req = webob.Request.blank('/v1.1/servers/1/metadata')
+ req.method = 'PUT'
+ req.content_type = "application/json"
+ expected = {
+ 'metadata': {
+ 'key10': 'value10',
+ 'key99': 'value99',
+ },
+ }
+ req.body = json.dumps(expected)
+ res = req.get_response(fakes.wsgi_app())
+ self.assertEqual(200, res.status_int)
+ res_dict = json.loads(res.body)
+ self.assertEqual(expected, res_dict)
+
+ def test_update_all_empty_container(self):
+ self.stubs.Set(nova.db.api, 'instance_metadata_update',
+ return_create_instance_metadata)
+ req = webob.Request.blank('/v1.1/servers/1/metadata')
+ req.method = 'PUT'
+ req.content_type = "application/json"
+ expected = {'metadata': {}}
+ req.body = json.dumps(expected)
+ res = req.get_response(fakes.wsgi_app())
+ self.assertEqual(200, res.status_int)
+ res_dict = json.loads(res.body)
+ self.assertEqual(expected, res_dict)
+
+ def test_update_all_malformed_container(self):
+ self.stubs.Set(nova.db.api, 'instance_metadata_update',
+ return_create_instance_metadata)
+ req = webob.Request.blank('/v1.1/servers/1/metadata')
+ req.method = 'PUT'
+ req.content_type = "application/json"
+ expected = {'meta': {}}
+ req.body = json.dumps(expected)
+ res = req.get_response(fakes.wsgi_app())
+ self.assertEqual(400, res.status_int)
+
+ def test_update_all_malformed_data(self):
+ self.stubs.Set(nova.db.api, 'instance_metadata_update',
+ return_create_instance_metadata)
+ req = webob.Request.blank('/v1.1/servers/1/metadata')
+ req.method = 'PUT'
+ req.content_type = "application/json"
+ expected = {'metadata': ['asdf']}
+ req.body = json.dumps(expected)
+ res = req.get_response(fakes.wsgi_app())
+ self.assertEqual(400, res.status_int)
+
+ def test_update_all_nonexistant_server(self):
+ self.stubs.Set(nova.db.api, 'instance_get', return_server_nonexistant)
+ req = webob.Request.blank('/v1.1/servers/100/metadata')
+ req.method = 'PUT'
+ req.content_type = "application/json"
+ req.body = json.dumps({'metadata': {'key10': 'value10'}})
+ res = req.get_response(fakes.wsgi_app())
+ self.assertEqual(404, res.status_int)
+
def test_update_item(self):
- self.stubs.Set(nova.db.api, 'instance_metadata_update_or_create',
+ self.stubs.Set(nova.db.api, 'instance_metadata_update',
return_create_instance_metadata)
- req = webob.Request.blank('/v1.1/servers/1/meta/key1')
- req.environ['api.version'] = '1.1'
+ req = webob.Request.blank('/v1.1/servers/1/metadata/key1')
req.method = 'PUT'
- req.body = '{"key1": "value1"}'
+ req.body = '{"meta": {"key1": "value1"}}'
req.headers["content-type"] = "application/json"
res = req.get_response(fakes.wsgi_app())
self.assertEqual(200, res.status_int)
self.assertEqual('application/json', res.headers['Content-Type'])
res_dict = json.loads(res.body)
- self.assertEqual('value1', res_dict['key1'])
+ expected = {'meta': {'key1': 'value1'}}
+ self.assertEqual(expected, res_dict)
+
+ def test_update_item_xml(self):
+ self.stubs.Set(nova.db.api, 'instance_metadata_update',
+ return_create_instance_metadata)
+ req = webob.Request.blank('/v1.1/servers/1/metadata/key9')
+ req.method = 'PUT'
+ req.accept = "application/json"
+ req.content_type = "application/xml"
+ req.body = """
+ <meta xmlns="http://docs.openstack.org/compute/api/v1.1"
+ key="key9">value9</meta>
+ """
+ res = req.get_response(fakes.wsgi_app())
+ self.assertEqual(200, res.status_int)
+ self.assertEqual('application/json', res.headers['Content-Type'])
+ res_dict = json.loads(res.body)
+ expected = {'meta': {'key9': 'value9'}}
+ self.assertEqual(expected, res_dict)
def test_update_item_nonexistant_server(self):
self.stubs.Set(nova.db.api, 'instance_get', return_server_nonexistant)
- req = webob.Request.blank('/v1.1/servers/asdf/100/key1')
- req.environ['api.version'] = '1.1'
+ req = webob.Request.blank('/v1.1/servers/asdf/metadata/key1')
req.method = 'PUT'
- req.body = '{"key1": "value1"}'
+ req.body = '{"meta":{"key1": "value1"}}'
req.headers["content-type"] = "application/json"
res = req.get_response(fakes.wsgi_app())
self.assertEqual(404, res.status_int)
def test_update_item_empty_body(self):
- self.stubs.Set(nova.db.api, 'instance_metadata_update_or_create',
+ self.stubs.Set(nova.db.api, 'instance_metadata_update',
return_create_instance_metadata)
- req = webob.Request.blank('/v1.1/servers/1/meta/key1')
- req.environ['api.version'] = '1.1'
+ req = webob.Request.blank('/v1.1/servers/1/metadata/key1')
req.method = 'PUT'
req.headers["content-type"] = "application/json"
res = req.get_response(fakes.wsgi_app())
self.assertEqual(400, res.status_int)
def test_update_item_too_many_keys(self):
- self.stubs.Set(nova.db.api, 'instance_metadata_update_or_create',
+ self.stubs.Set(nova.db.api, 'instance_metadata_update',
return_create_instance_metadata)
- req = webob.Request.blank('/v1.1/servers/1/meta/key1')
- req.environ['api.version'] = '1.1'
+ req = webob.Request.blank('/v1.1/servers/1/metadata/key1')
req.method = 'PUT'
- req.body = '{"key1": "value1", "key2": "value2"}'
+ req.body = '{"meta": {"key1": "value1", "key2": "value2"}}'
req.headers["content-type"] = "application/json"
res = req.get_response(fakes.wsgi_app())
self.assertEqual(400, res.status_int)
def test_update_item_body_uri_mismatch(self):
- self.stubs.Set(nova.db.api, 'instance_metadata_update_or_create',
+ self.stubs.Set(nova.db.api, 'instance_metadata_update',
return_create_instance_metadata)
- req = webob.Request.blank('/v1.1/servers/1/meta/bad')
- req.environ['api.version'] = '1.1'
+ req = webob.Request.blank('/v1.1/servers/1/metadata/bad')
req.method = 'PUT'
- req.body = '{"key1": "value1"}'
+ req.body = '{"meta": {"key1": "value1"}}'
req.headers["content-type"] = "application/json"
res = req.get_response(fakes.wsgi_app())
self.assertEqual(400, res.status_int)
def test_too_many_metadata_items_on_create(self):
- self.stubs.Set(nova.db.api, 'instance_metadata_update_or_create',
+ self.stubs.Set(nova.db.api, 'instance_metadata_update',
return_create_instance_metadata)
data = {"metadata": {}}
for num in range(FLAGS.quota_metadata_items + 1):
data['metadata']['key%i' % num] = "blah"
json_string = str(data).replace("\'", "\"")
- req = webob.Request.blank('/v1.1/servers/1/meta')
- req.environ['api.version'] = '1.1'
+ req = webob.Request.blank('/v1.1/servers/1/metadata')
req.method = 'POST'
req.body = json_string
req.headers["content-type"] = "application/json"
@@ -259,12 +420,11 @@ class ServerMetaDataTest(test.TestCase):
self.assertEqual(400, res.status_int)
def test_to_many_metadata_items_on_update_item(self):
- self.stubs.Set(nova.db.api, 'instance_metadata_update_or_create',
+ self.stubs.Set(nova.db.api, 'instance_metadata_update',
return_create_instance_metadata_max)
- req = webob.Request.blank('/v1.1/servers/1/meta/key1')
- req.environ['api.version'] = '1.1'
+ req = webob.Request.blank('/v1.1/servers/1/metadata/key1')
req.method = 'PUT'
- req.body = '{"a new key": "a new value"}'
+ req.body = '{"meta": {"a new key": "a new value"}}'
req.headers["content-type"] = "application/json"
res = req.get_response(fakes.wsgi_app())
self.assertEqual(400, res.status_int)
diff --git a/nova/tests/api/openstack/test_servers.py b/nova/tests/api/openstack/test_servers.py
index 4d42972c1..b6342ae2f 100644
--- a/nova/tests/api/openstack/test_servers.py
+++ b/nova/tests/api/openstack/test_servers.py
@@ -26,7 +26,6 @@ import webob
from nova import context
from nova import db
from nova import exception
-from nova import flags
from nova import test
from nova import utils
import nova.api.openstack
@@ -46,10 +45,6 @@ from nova.tests.api.openstack import common
from nova.tests.api.openstack import fakes
-FLAGS = flags.FLAGS
-FLAGS.verbose = True
-
-
FAKE_UUID = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa'
@@ -235,12 +230,14 @@ class ServersTest(test.TestCase):
def setUp(self):
self.maxDiff = None
super(ServersTest, self).setUp()
+ self.flags(verbose=True)
fakes.stub_out_networking(self.stubs)
fakes.stub_out_rate_limiting(self.stubs)
fakes.stub_out_key_pair_funcs(self.stubs)
fakes.stub_out_image_service(self.stubs)
self.stubs.Set(utils, 'gen_uuid', fake_gen_uuid)
- self.stubs.Set(nova.db.api, 'instance_get_all', return_servers)
+ self.stubs.Set(nova.db.api, 'instance_get_all_by_filters',
+ return_servers)
self.stubs.Set(nova.db.api, 'instance_get', return_server_by_id)
self.stubs.Set(nova.db, 'instance_get_by_uuid',
return_server_by_uuid)
@@ -260,17 +257,6 @@ class ServersTest(test.TestCase):
self.stubs.Set(nova.compute.API, "get_diagnostics", fake_compute_api)
self.stubs.Set(nova.compute.API, "get_actions", fake_compute_api)
- fakes.stub_out_glance(self.stubs)
- fakes.stub_out_compute_api_snapshot(self.stubs)
- service_class = 'nova.image.glance.GlanceImageService'
- self.service = utils.import_object(service_class)
- self.context = context.RequestContext(1, None)
- self.service.delete_all()
- self.sent_to_glance = {}
- fakes.stub_out_glance_add_image(self.stubs, self.sent_to_glance)
-
- self.allow_admin = FLAGS.allow_admin_api
-
self.webreq = common.webob_factory('/v1.0/servers')
def test_get_server_by_id(self):
@@ -767,7 +753,7 @@ class ServersTest(test.TestCase):
self.assertEquals(ip.getAttribute('addr'), private)
def test_get_server_by_id_with_addresses_v1_1(self):
- FLAGS.use_ipv6 = True
+ self.flags(use_ipv6=True)
interfaces = [
{
'network': {'label': 'network_1'},
@@ -811,7 +797,7 @@ class ServersTest(test.TestCase):
self.assertEqual(addresses, expected)
def test_get_server_by_id_with_addresses_v1_1_ipv6_disabled(self):
- FLAGS.use_ipv6 = False
+ self.flags(use_ipv6=False)
interfaces = [
{
'network': {'label': 'network_1'},
@@ -854,7 +840,7 @@ class ServersTest(test.TestCase):
self.assertEqual(addresses, expected)
def test_get_server_addresses_v1_1(self):
- FLAGS.use_ipv6 = True
+ self.flags(use_ipv6=True)
interfaces = [
{
'network': {'label': 'network_1'},
@@ -905,7 +891,7 @@ class ServersTest(test.TestCase):
self.assertEqual(res_dict, expected)
def test_get_server_addresses_single_network_v1_1(self):
- FLAGS.use_ipv6 = True
+ self.flags(use_ipv6=True)
interfaces = [
{
'network': {'label': 'network_1'},
@@ -1113,6 +1099,277 @@ class ServersTest(test.TestCase):
self.assertEqual(res.status_int, 400)
self.assertTrue(res.body.find('marker param') > -1)
+ def test_get_servers_with_bad_option_v1_0(self):
+ # 1.0 API ignores unknown options
+ def fake_get_all(compute_self, context, search_opts=None):
+ return [stub_instance(100)]
+
+ self.stubs.Set(nova.compute.API, 'get_all', fake_get_all)
+
+ req = webob.Request.blank('/v1.0/servers?unknownoption=whee')
+ res = req.get_response(fakes.wsgi_app())
+ self.assertEqual(res.status_int, 200)
+ servers = json.loads(res.body)['servers']
+ self.assertEqual(len(servers), 1)
+ self.assertEqual(servers[0]['id'], 100)
+
+ def test_get_servers_with_bad_option_v1_1(self):
+ # 1.1 API also ignores unknown options
+ def fake_get_all(compute_self, context, search_opts=None):
+ return [stub_instance(100)]
+
+ self.stubs.Set(nova.compute.API, 'get_all', fake_get_all)
+
+ req = webob.Request.blank('/v1.1/servers?unknownoption=whee')
+ res = req.get_response(fakes.wsgi_app())
+ self.assertEqual(res.status_int, 200)
+ servers = json.loads(res.body)['servers']
+ self.assertEqual(len(servers), 1)
+ self.assertEqual(servers[0]['id'], 100)
+
+ def test_get_servers_allows_image_v1_1(self):
+ def fake_get_all(compute_self, context, search_opts=None):
+ self.assertNotEqual(search_opts, None)
+ self.assertTrue('image' in search_opts)
+ self.assertEqual(search_opts['image'], '12345')
+ return [stub_instance(100)]
+
+ self.stubs.Set(nova.compute.API, 'get_all', fake_get_all)
+ self.flags(allow_admin_api=False)
+
+ req = webob.Request.blank('/v1.1/servers?image=12345')
+ res = req.get_response(fakes.wsgi_app())
+ # The following assert will fail if either of the asserts in
+ # fake_get_all() fail
+ self.assertEqual(res.status_int, 200)
+ servers = json.loads(res.body)['servers']
+ self.assertEqual(len(servers), 1)
+ self.assertEqual(servers[0]['id'], 100)
+
+ def test_get_servers_allows_flavor_v1_1(self):
+ def fake_get_all(compute_self, context, search_opts=None):
+ self.assertNotEqual(search_opts, None)
+ self.assertTrue('flavor' in search_opts)
+ # flavor is an integer ID
+ self.assertEqual(search_opts['flavor'], '12345')
+ return [stub_instance(100)]
+
+ self.stubs.Set(nova.compute.API, 'get_all', fake_get_all)
+ self.flags(allow_admin_api=False)
+
+ req = webob.Request.blank('/v1.1/servers?flavor=12345')
+ res = req.get_response(fakes.wsgi_app())
+ # The following assert will fail if either of the asserts in
+ # fake_get_all() fail
+ self.assertEqual(res.status_int, 200)
+ servers = json.loads(res.body)['servers']
+ self.assertEqual(len(servers), 1)
+ self.assertEqual(servers[0]['id'], 100)
+
+ def test_get_servers_allows_status_v1_1(self):
+ def fake_get_all(compute_self, context, search_opts=None):
+ self.assertNotEqual(search_opts, None)
+ self.assertTrue('state' in search_opts)
+ self.assertEqual(set(search_opts['state']),
+ set([power_state.RUNNING, power_state.BLOCKED]))
+ return [stub_instance(100)]
+
+ self.stubs.Set(nova.compute.API, 'get_all', fake_get_all)
+ self.flags(allow_admin_api=False)
+
+ req = webob.Request.blank('/v1.1/servers?status=active')
+ res = req.get_response(fakes.wsgi_app())
+ # The following assert will fail if either of the asserts in
+ # fake_get_all() fail
+ self.assertEqual(res.status_int, 200)
+ servers = json.loads(res.body)['servers']
+ self.assertEqual(len(servers), 1)
+ self.assertEqual(servers[0]['id'], 100)
+
+ def test_get_servers_invalid_status_v1_1(self):
+ """Test getting servers by invalid status"""
+
+ self.flags(allow_admin_api=False)
+
+ req = webob.Request.blank('/v1.1/servers?status=running')
+ res = req.get_response(fakes.wsgi_app())
+ # The following assert will fail if either of the asserts in
+ # fake_get_all() fail
+ self.assertEqual(res.status_int, 400)
+ self.assertTrue(res.body.find('Invalid server status') > -1)
+
+ def test_get_servers_allows_name_v1_1(self):
+ def fake_get_all(compute_self, context, search_opts=None):
+ self.assertNotEqual(search_opts, None)
+ self.assertTrue('name' in search_opts)
+ self.assertEqual(search_opts['name'], 'whee.*')
+ return [stub_instance(100)]
+
+ self.stubs.Set(nova.compute.API, 'get_all', fake_get_all)
+ self.flags(allow_admin_api=False)
+
+ req = webob.Request.blank('/v1.1/servers?name=whee.*')
+ res = req.get_response(fakes.wsgi_app())
+ # The following assert will fail if either of the asserts in
+ # fake_get_all() fail
+ self.assertEqual(res.status_int, 200)
+ servers = json.loads(res.body)['servers']
+ self.assertEqual(len(servers), 1)
+ self.assertEqual(servers[0]['id'], 100)
+
+ def test_get_servers_unknown_or_admin_options1(self):
+ """Test getting servers by admin-only or unknown options.
+ This tests when admin_api is off. Make sure the admin and
+ unknown options are stripped before they get to
+ compute_api.get_all()
+ """
+
+ self.flags(allow_admin_api=False)
+
+ def fake_get_all(compute_self, context, search_opts=None):
+ self.assertNotEqual(search_opts, None)
+ # Allowed by user
+ self.assertTrue('name' in search_opts)
+ self.assertTrue('status' in search_opts)
+ # Allowed only by admins with admin API on
+ self.assertFalse('ip' in search_opts)
+ self.assertFalse('unknown_option' in search_opts)
+ return [stub_instance(100)]
+
+ self.stubs.Set(nova.compute.API, 'get_all', fake_get_all)
+
+ query_str = "name=foo&ip=10.*&status=active&unknown_option=meow"
+ req = webob.Request.blank('/v1.1/servers?%s' % query_str)
+ # Request admin context
+ context = nova.context.RequestContext('testuser', 'testproject',
+ is_admin=True)
+ res = req.get_response(fakes.wsgi_app(fake_auth_context=context))
+ # The following assert will fail if either of the asserts in
+ # fake_get_all() fail
+ self.assertEqual(res.status_int, 200)
+ servers = json.loads(res.body)['servers']
+ self.assertEqual(len(servers), 1)
+ self.assertEqual(servers[0]['id'], 100)
+
+ def test_get_servers_unknown_or_admin_options2(self):
+ """Test getting servers by admin-only or unknown options.
+ This tests when admin_api is on, but context is a user.
+ Make sure the admin and unknown options are stripped before
+ they get to compute_api.get_all()
+ """
+
+ self.flags(allow_admin_api=True)
+
+ def fake_get_all(compute_self, context, search_opts=None):
+ self.assertNotEqual(search_opts, None)
+ # Allowed by user
+ self.assertTrue('name' in search_opts)
+ self.assertTrue('status' in search_opts)
+ # Allowed only by admins with admin API on
+ self.assertFalse('ip' in search_opts)
+ self.assertFalse('unknown_option' in search_opts)
+ return [stub_instance(100)]
+
+ self.stubs.Set(nova.compute.API, 'get_all', fake_get_all)
+
+ query_str = "name=foo&ip=10.*&status=active&unknown_option=meow"
+ req = webob.Request.blank('/v1.1/servers?%s' % query_str)
+ # Request admin context
+ context = nova.context.RequestContext('testuser', 'testproject',
+ is_admin=False)
+ res = req.get_response(fakes.wsgi_app(fake_auth_context=context))
+ # The following assert will fail if either of the asserts in
+ # fake_get_all() fail
+ self.assertEqual(res.status_int, 200)
+ servers = json.loads(res.body)['servers']
+ self.assertEqual(len(servers), 1)
+ self.assertEqual(servers[0]['id'], 100)
+
+ def test_get_servers_unknown_or_admin_options3(self):
+ """Test getting servers by admin-only or unknown options.
+ This tests when admin_api is on and context is admin.
+ All options should be passed through to compute_api.get_all()
+ """
+
+ self.flags(allow_admin_api=True)
+
+ def fake_get_all(compute_self, context, search_opts=None):
+ self.assertNotEqual(search_opts, None)
+ # Allowed by user
+ self.assertTrue('name' in search_opts)
+ self.assertTrue('status' in search_opts)
+ # Allowed only by admins with admin API on
+ self.assertTrue('ip' in search_opts)
+ self.assertTrue('unknown_option' in search_opts)
+ return [stub_instance(100)]
+
+ self.stubs.Set(nova.compute.API, 'get_all', fake_get_all)
+
+ query_str = "name=foo&ip=10.*&status=active&unknown_option=meow"
+ req = webob.Request.blank('/v1.1/servers?%s' % query_str)
+ # Request admin context
+ context = nova.context.RequestContext('testuser', 'testproject',
+ is_admin=True)
+ res = req.get_response(fakes.wsgi_app(fake_auth_context=context))
+ # The following assert will fail if either of the asserts in
+ # fake_get_all() fail
+ self.assertEqual(res.status_int, 200)
+ servers = json.loads(res.body)['servers']
+ self.assertEqual(len(servers), 1)
+ self.assertEqual(servers[0]['id'], 100)
+
+ def test_get_servers_admin_allows_ip_v1_1(self):
+ """Test getting servers by ip with admin_api enabled and
+ admin context
+ """
+ self.flags(allow_admin_api=True)
+
+ def fake_get_all(compute_self, context, search_opts=None):
+ self.assertNotEqual(search_opts, None)
+ self.assertTrue('ip' in search_opts)
+ self.assertEqual(search_opts['ip'], '10\..*')
+ return [stub_instance(100)]
+
+ self.stubs.Set(nova.compute.API, 'get_all', fake_get_all)
+
+ req = webob.Request.blank('/v1.1/servers?ip=10\..*')
+ # Request admin context
+ context = nova.context.RequestContext('testuser', 'testproject',
+ is_admin=True)
+ res = req.get_response(fakes.wsgi_app(fake_auth_context=context))
+ # The following assert will fail if either of the asserts in
+ # fake_get_all() fail
+ self.assertEqual(res.status_int, 200)
+ servers = json.loads(res.body)['servers']
+ self.assertEqual(len(servers), 1)
+ self.assertEqual(servers[0]['id'], 100)
+
+ def test_get_servers_admin_allows_ip6_v1_1(self):
+ """Test getting servers by ip6 with admin_api enabled and
+ admin context
+ """
+ self.flags(allow_admin_api=True)
+
+ def fake_get_all(compute_self, context, search_opts=None):
+ self.assertNotEqual(search_opts, None)
+ self.assertTrue('ip6' in search_opts)
+ self.assertEqual(search_opts['ip6'], 'ffff.*')
+ return [stub_instance(100)]
+
+ self.stubs.Set(nova.compute.API, 'get_all', fake_get_all)
+
+ req = webob.Request.blank('/v1.1/servers?ip6=ffff.*')
+ # Request admin context
+ context = nova.context.RequestContext('testuser', 'testproject',
+ is_admin=True)
+ res = req.get_response(fakes.wsgi_app(fake_auth_context=context))
+ # The following assert will fail if either of the asserts in
+ # fake_get_all() fail
+ self.assertEqual(res.status_int, 200)
+ servers = json.loads(res.body)['servers']
+ self.assertEqual(len(servers), 1)
+ self.assertEqual(servers[0]['id'], 100)
+
def _setup_for_create_instance(self):
"""Shared implementation for tests below that create instance"""
def instance_create(context, inst):
@@ -1174,7 +1431,7 @@ class ServersTest(test.TestCase):
res = req.get_response(fakes.wsgi_app())
- self.assertEqual(res.status_int, 200)
+ self.assertEqual(res.status_int, 202)
server = json.loads(res.body)['server']
self.assertEqual(16, len(server['adminPass']))
self.assertEqual('server_test', server['name'])
@@ -1371,7 +1628,7 @@ class ServersTest(test.TestCase):
res = req.get_response(fakes.wsgi_app())
- self.assertEqual(res.status_int, 200)
+ self.assertEqual(res.status_int, 202)
server = json.loads(res.body)['server']
self.assertEqual(16, len(server['adminPass']))
self.assertEqual(1, server['id'])
@@ -1466,7 +1723,7 @@ class ServersTest(test.TestCase):
res = req.get_response(fakes.wsgi_app())
- self.assertEqual(res.status_int, 200)
+ self.assertEqual(res.status_int, 202)
server = json.loads(res.body)['server']
self.assertEqual(expected_flavor, server['flavor'])
self.assertEqual(expected_image, server['image'])
@@ -1511,7 +1768,7 @@ class ServersTest(test.TestCase):
req.body = json.dumps(body)
req.headers['content-type'] = "application/json"
res = req.get_response(fakes.wsgi_app())
- self.assertEqual(res.status_int, 200)
+ self.assertEqual(res.status_int, 202)
server = json.loads(res.body)['server']
self.assertEqual(server['adminPass'], body['server']['adminPass'])
@@ -1680,6 +1937,7 @@ class ServersTest(test.TestCase):
def test_get_all_server_details_v1_0(self):
req = webob.Request.blank('/v1.0/servers/detail')
res = req.get_response(fakes.wsgi_app())
+ self.assertEqual(res.status_int, 200)
res_dict = json.loads(res.body)
for i, s in enumerate(res_dict['servers']):
@@ -1735,7 +1993,7 @@ class ServersTest(test.TestCase):
return [stub_instance(i, 'fake', 'fake', None, None, i % 2)
for i in xrange(5)]
- self.stubs.Set(nova.db.api, 'instance_get_all_by_project',
+ self.stubs.Set(nova.db.api, 'instance_get_all_by_filters',
return_servers_with_host)
req = webob.Request.blank('/v1.0/servers/detail')
@@ -1756,339 +2014,67 @@ class ServersTest(test.TestCase):
def test_server_pause(self):
self.flags(allow_admin_api=True)
- body = dict(server=dict(
- name='server_test', imageId=2, flavorId=2, metadata={},
- personality={}))
req = webob.Request.blank('/v1.0/servers/1/pause')
req.method = 'POST'
req.content_type = 'application/json'
- req.body = json.dumps(body)
res = req.get_response(fakes.wsgi_app())
self.assertEqual(res.status_int, 202)
def test_server_unpause(self):
self.flags(allow_admin_api=True)
- body = dict(server=dict(
- name='server_test', imageId=2, flavorId=2, metadata={},
- personality={}))
req = webob.Request.blank('/v1.0/servers/1/unpause')
req.method = 'POST'
req.content_type = 'application/json'
- req.body = json.dumps(body)
res = req.get_response(fakes.wsgi_app())
self.assertEqual(res.status_int, 202)
def test_server_suspend(self):
self.flags(allow_admin_api=True)
- body = dict(server=dict(
- name='server_test', imageId=2, flavorId=2, metadata={},
- personality={}))
req = webob.Request.blank('/v1.0/servers/1/suspend')
req.method = 'POST'
req.content_type = 'application/json'
- req.body = json.dumps(body)
res = req.get_response(fakes.wsgi_app())
self.assertEqual(res.status_int, 202)
def test_server_resume(self):
self.flags(allow_admin_api=True)
- body = dict(server=dict(
- name='server_test', imageId=2, flavorId=2, metadata={},
- personality={}))
req = webob.Request.blank('/v1.0/servers/1/resume')
req.method = 'POST'
req.content_type = 'application/json'
- req.body = json.dumps(body)
res = req.get_response(fakes.wsgi_app())
self.assertEqual(res.status_int, 202)
def test_server_reset_network(self):
self.flags(allow_admin_api=True)
- body = dict(server=dict(
- name='server_test', imageId=2, flavorId=2, metadata={},
- personality={}))
req = webob.Request.blank('/v1.0/servers/1/reset_network')
req.method = 'POST'
req.content_type = 'application/json'
- req.body = json.dumps(body)
res = req.get_response(fakes.wsgi_app())
self.assertEqual(res.status_int, 202)
def test_server_inject_network_info(self):
self.flags(allow_admin_api=True)
- body = dict(server=dict(
- name='server_test', imageId=2, flavorId=2, metadata={},
- personality={}))
req = webob.Request.blank(
'/v1.0/servers/1/inject_network_info')
req.method = 'POST'
req.content_type = 'application/json'
- req.body = json.dumps(body)
res = req.get_response(fakes.wsgi_app())
self.assertEqual(res.status_int, 202)
def test_server_diagnostics(self):
+ self.flags(allow_admin_api=False)
req = webob.Request.blank("/v1.0/servers/1/diagnostics")
req.method = "GET"
res = req.get_response(fakes.wsgi_app())
self.assertEqual(res.status_int, 404)
def test_server_actions(self):
+ self.flags(allow_admin_api=False)
req = webob.Request.blank("/v1.0/servers/1/actions")
req.method = "GET"
res = req.get_response(fakes.wsgi_app())
self.assertEqual(res.status_int, 404)
- def test_server_change_password(self):
- body = {'changePassword': {'adminPass': '1234pass'}}
- req = webob.Request.blank('/v1.0/servers/1/action')
- req.method = 'POST'
- req.content_type = 'application/json'
- req.body = json.dumps(body)
- res = req.get_response(fakes.wsgi_app())
- self.assertEqual(res.status_int, 501)
-
- def test_server_change_password_xml(self):
- req = webob.Request.blank('/v1.0/servers/1/action')
- req.method = 'POST'
- req.content_type = 'application/xml'
- req.body = '<changePassword adminPass="1234pass">'
-# res = req.get_response(fakes.wsgi_app())
-# self.assertEqual(res.status_int, 501)
-
- def test_server_change_password_v1_1(self):
- mock_method = MockSetAdminPassword()
- self.stubs.Set(nova.compute.api.API, 'set_admin_password', mock_method)
- body = {'changePassword': {'adminPass': '1234pass'}}
- req = webob.Request.blank('/v1.1/servers/1/action')
- req.method = 'POST'
- req.content_type = 'application/json'
- req.body = json.dumps(body)
- res = req.get_response(fakes.wsgi_app())
- self.assertEqual(res.status_int, 202)
- self.assertEqual(mock_method.instance_id, '1')
- self.assertEqual(mock_method.password, '1234pass')
-
- def test_server_change_password_bad_request_v1_1(self):
- body = {'changePassword': {'pass': '12345'}}
- req = webob.Request.blank('/v1.1/servers/1/action')
- req.method = 'POST'
- req.content_type = 'application/json'
- req.body = json.dumps(body)
- res = req.get_response(fakes.wsgi_app())
- self.assertEqual(res.status_int, 400)
-
- def test_server_change_password_empty_string_v1_1(self):
- body = {'changePassword': {'adminPass': ''}}
- req = webob.Request.blank('/v1.1/servers/1/action')
- req.method = 'POST'
- req.content_type = 'application/json'
- req.body = json.dumps(body)
- res = req.get_response(fakes.wsgi_app())
- self.assertEqual(res.status_int, 400)
-
- def test_server_change_password_none_v1_1(self):
- body = {'changePassword': {'adminPass': None}}
- req = webob.Request.blank('/v1.1/servers/1/action')
- req.method = 'POST'
- req.content_type = 'application/json'
- req.body = json.dumps(body)
- res = req.get_response(fakes.wsgi_app())
- self.assertEqual(res.status_int, 400)
-
- def test_server_change_password_not_a_string_v1_1(self):
- body = {'changePassword': {'adminPass': 1234}}
- req = webob.Request.blank('/v1.1/servers/1/action')
- req.method = 'POST'
- req.content_type = 'application/json'
- req.body = json.dumps(body)
- res = req.get_response(fakes.wsgi_app())
- self.assertEqual(res.status_int, 400)
-
- def test_server_reboot(self):
- body = dict(server=dict(
- name='server_test', imageId=2, flavorId=2, metadata={},
- personality={}))
- req = webob.Request.blank('/v1.0/servers/1/action')
- req.method = 'POST'
- req.content_type = 'application/json'
- req.body = json.dumps(body)
- res = req.get_response(fakes.wsgi_app())
-
- def test_server_rebuild_accepted(self):
- body = {
- "rebuild": {
- "imageId": 2,
- },
- }
-
- req = webob.Request.blank('/v1.0/servers/1/action')
- req.method = 'POST'
- req.content_type = 'application/json'
- req.body = json.dumps(body)
-
- res = req.get_response(fakes.wsgi_app())
- self.assertEqual(res.status_int, 202)
- self.assertEqual(res.body, "")
-
- def test_server_rebuild_rejected_when_building(self):
- body = {
- "rebuild": {
- "imageId": 2,
- },
- }
-
- state = power_state.BUILDING
- new_return_server = return_server_with_power_state(state)
- self.stubs.Set(nova.db.api, 'instance_get', new_return_server)
- self.stubs.Set(nova.db, 'instance_get_by_uuid',
- return_server_with_uuid_and_power_state(state))
-
- req = webob.Request.blank('/v1.0/servers/1/action')
- req.method = 'POST'
- req.content_type = 'application/json'
- req.body = json.dumps(body)
-
- res = req.get_response(fakes.wsgi_app())
- self.assertEqual(res.status_int, 409)
-
- def test_server_rebuild_bad_entity(self):
- body = {
- "rebuild": {
- },
- }
-
- req = webob.Request.blank('/v1.0/servers/1/action')
- req.method = 'POST'
- req.content_type = 'application/json'
- req.body = json.dumps(body)
-
- res = req.get_response(fakes.wsgi_app())
- self.assertEqual(res.status_int, 400)
-
- def test_server_rebuild_accepted_minimum_v1_1(self):
- body = {
- "rebuild": {
- "imageRef": "http://localhost/images/2",
- },
- }
-
- req = webob.Request.blank('/v1.1/servers/1/action')
- req.method = 'POST'
- req.content_type = 'application/json'
- req.body = json.dumps(body)
-
- res = req.get_response(fakes.wsgi_app())
- self.assertEqual(res.status_int, 202)
-
- def test_server_rebuild_rejected_when_building_v1_1(self):
- body = {
- "rebuild": {
- "imageRef": "http://localhost/images/2",
- },
- }
-
- state = power_state.BUILDING
- new_return_server = return_server_with_power_state(state)
- self.stubs.Set(nova.db.api, 'instance_get', new_return_server)
- self.stubs.Set(nova.db, 'instance_get_by_uuid',
- return_server_with_uuid_and_power_state(state))
-
- req = webob.Request.blank('/v1.1/servers/1/action')
- req.method = 'POST'
- req.content_type = 'application/json'
- req.body = json.dumps(body)
-
- res = req.get_response(fakes.wsgi_app())
- self.assertEqual(res.status_int, 409)
-
- def test_server_rebuild_accepted_with_metadata_v1_1(self):
- body = {
- "rebuild": {
- "imageRef": "http://localhost/images/2",
- "metadata": {
- "new": "metadata",
- },
- },
- }
-
- req = webob.Request.blank('/v1.1/servers/1/action')
- req.method = 'POST'
- req.content_type = 'application/json'
- req.body = json.dumps(body)
-
- res = req.get_response(fakes.wsgi_app())
- self.assertEqual(res.status_int, 202)
-
- def test_server_rebuild_accepted_with_bad_metadata_v1_1(self):
- body = {
- "rebuild": {
- "imageRef": "http://localhost/images/2",
- "metadata": "stack",
- },
- }
-
- req = webob.Request.blank('/v1.1/servers/1/action')
- req.method = 'POST'
- req.content_type = 'application/json'
- req.body = json.dumps(body)
-
- res = req.get_response(fakes.wsgi_app())
- self.assertEqual(res.status_int, 400)
-
- def test_server_rebuild_bad_entity_v1_1(self):
- body = {
- "rebuild": {
- "imageId": 2,
- },
- }
-
- req = webob.Request.blank('/v1.1/servers/1/action')
- req.method = 'POST'
- req.content_type = 'application/json'
- req.body = json.dumps(body)
-
- res = req.get_response(fakes.wsgi_app())
- self.assertEqual(res.status_int, 400)
-
- def test_server_rebuild_bad_personality_v1_1(self):
- body = {
- "rebuild": {
- "imageRef": "http://localhost/images/2",
- "personality": [{
- "path": "/path/to/file",
- "contents": "INVALID b64",
- }]
- },
- }
-
- req = webob.Request.blank('/v1.1/servers/1/action')
- req.method = 'POST'
- req.content_type = 'application/json'
- req.body = json.dumps(body)
-
- res = req.get_response(fakes.wsgi_app())
- self.assertEqual(res.status_int, 400)
-
- def test_server_rebuild_personality_v1_1(self):
- body = {
- "rebuild": {
- "imageRef": "http://localhost/images/2",
- "personality": [{
- "path": "/path/to/file",
- "contents": base64.b64encode("Test String"),
- }]
- },
- }
-
- req = webob.Request.blank('/v1.1/servers/1/action')
- req.method = 'POST'
- req.content_type = 'application/json'
- req.body = json.dumps(body)
-
- res = req.get_response(fakes.wsgi_app())
- self.assertEqual(res.status_int, 202)
-
def test_delete_server_instance(self):
req = webob.Request.blank('/v1.0/servers/1')
req.method = 'DELETE'
@@ -2156,199 +2142,6 @@ class ServersTest(test.TestCase):
self.assertEqual(res.status_int, 204)
self.assertEqual(self.server_delete_called, True)
- def test_resize_server(self):
- req = self.webreq('/1/action', 'POST', dict(resize=dict(flavorId=3)))
-
- self.resize_called = False
-
- def resize_mock(*args):
- self.resize_called = True
-
- self.stubs.Set(nova.compute.api.API, 'resize', resize_mock)
-
- res = req.get_response(fakes.wsgi_app())
- self.assertEqual(res.status_int, 202)
- self.assertEqual(self.resize_called, True)
-
- def test_resize_server_v11(self):
- req = webob.Request.blank('/v1.1/servers/1/action')
- req.content_type = 'application/json'
- req.method = 'POST'
- body_dict = {
- "resize": {
- "flavorRef": 3,
- },
- }
- req.body = json.dumps(body_dict)
-
- self.resize_called = False
-
- def resize_mock(*args):
- self.resize_called = True
-
- self.stubs.Set(nova.compute.api.API, 'resize', resize_mock)
-
- res = req.get_response(fakes.wsgi_app())
- self.assertEqual(res.status_int, 202)
- self.assertEqual(self.resize_called, True)
-
- def test_resize_bad_flavor_data(self):
- req = self.webreq('/1/action', 'POST', {"resize": "bad_data"})
-
- self.resize_called = False
-
- def resize_mock(*args):
- self.resize_called = True
-
- self.stubs.Set(nova.compute.api.API, 'resize', resize_mock)
-
- res = req.get_response(fakes.wsgi_app())
- self.assertEqual(res.status_int, 400)
- self.assertEqual(self.resize_called, False)
-
- def test_resize_invalid_flavorid(self):
- req = self.webreq('/1/action', 'POST', {"resize": {"flavorId": 300}})
- res = req.get_response(fakes.wsgi_app())
- self.assertEqual(res.status_int, 400)
-
- def test_resize_nonint_flavorid(self):
- req = self.webreq('/1/action', 'POST', {"resize": {"flavorId": "a"}})
- res = req.get_response(fakes.wsgi_app())
- self.assertEqual(res.status_int, 400)
-
- def test_resize_invalid_flavorid_v1_1(self):
- req = webob.Request.blank('/v1.1/servers/1/action')
- req.content_type = 'application/json'
- req.method = 'POST'
- resize_body = {
- "resize": {
- "image": {
- "id": 300,
- },
- },
- }
- req.body = json.dumps(resize_body)
- res = req.get_response(fakes.wsgi_app())
- self.assertEqual(res.status_int, 400)
-
- def test_resize_nonint_flavorid_v1_1(self):
- req = webob.Request.blank('/v1.1/servers/1/action')
- req.content_type = 'application/json'
- req.method = 'POST'
- resize_body = {
- "resize": {
- "image": {
- "id": "a",
- },
- },
- }
- req.body = json.dumps(resize_body)
- res = req.get_response(fakes.wsgi_app())
- self.assertEqual(res.status_int, 400)
-
- def test_resize_raises_fails(self):
- req = self.webreq('/1/action', 'POST', dict(resize=dict(flavorId=3)))
-
- def resize_mock(*args):
- raise Exception("An error occurred.")
-
- self.stubs.Set(nova.compute.api.API, 'resize', resize_mock)
-
- res = req.get_response(fakes.wsgi_app())
- self.assertEqual(res.status_int, 500)
-
- def test_resized_server_has_correct_status(self):
- req = self.webreq('/1', 'GET')
-
- def fake_migration_get(*args):
- return {}
-
- self.stubs.Set(nova.db, 'migration_get_by_instance_and_status',
- fake_migration_get)
- res = req.get_response(fakes.wsgi_app())
- body = json.loads(res.body)
- self.assertEqual(body['server']['status'], 'RESIZE-CONFIRM')
-
- def test_confirm_resize_server(self):
- req = self.webreq('/1/action', 'POST', dict(confirmResize=None))
-
- self.resize_called = False
-
- def confirm_resize_mock(*args):
- self.resize_called = True
-
- self.stubs.Set(nova.compute.api.API, 'confirm_resize',
- confirm_resize_mock)
-
- res = req.get_response(fakes.wsgi_app())
- self.assertEqual(res.status_int, 204)
- self.assertEqual(self.resize_called, True)
-
- def test_confirm_resize_server_fails(self):
- req = self.webreq('/1/action', 'POST', dict(confirmResize=None))
-
- def confirm_resize_mock(*args):
- raise Exception("An error occurred.")
-
- self.stubs.Set(nova.compute.api.API, 'confirm_resize',
- confirm_resize_mock)
-
- res = req.get_response(fakes.wsgi_app())
- self.assertEqual(res.status_int, 400)
-
- def test_revert_resize_server(self):
- req = self.webreq('/1/action', 'POST', dict(revertResize=None))
-
- self.resize_called = False
-
- def revert_resize_mock(*args):
- self.resize_called = True
-
- self.stubs.Set(nova.compute.api.API, 'revert_resize',
- revert_resize_mock)
-
- res = req.get_response(fakes.wsgi_app())
- self.assertEqual(res.status_int, 202)
- self.assertEqual(self.resize_called, True)
-
- def test_revert_resize_server_fails(self):
- req = self.webreq('/1/action', 'POST', dict(revertResize=None))
-
- def revert_resize_mock(*args):
- raise Exception("An error occurred.")
-
- self.stubs.Set(nova.compute.api.API, 'revert_resize',
- revert_resize_mock)
-
- res = req.get_response(fakes.wsgi_app())
- self.assertEqual(res.status_int, 400)
-
- def test_migrate_server(self):
- """This is basically the same as resize, only we provide the `migrate`
- attribute in the body's dict.
- """
- req = self.webreq('/1/migrate', 'POST')
-
- FLAGS.allow_admin_api = True
- self.resize_called = False
-
- def resize_mock(*args):
- self.resize_called = True
-
- self.stubs.Set(nova.compute.api.API, 'resize', resize_mock)
-
- res = req.get_response(fakes.wsgi_app())
- self.assertEqual(res.status_int, 202)
- self.assertEqual(self.resize_called, True)
-
- def test_migrate_server_no_admin_api_fails(self):
- req = self.webreq('/1/migrate', 'POST')
-
- FLAGS.allow_admin_api = False
-
- res = req.get_response(fakes.wsgi_app())
- self.assertEqual(res.status_int, 404)
-
def test_shutdown_status(self):
new_server = return_server_with_power_state(power_state.SHUTDOWN)
self.stubs.Set(nova.db.api, 'instance_get', new_server)
@@ -2367,268 +2160,6 @@ class ServersTest(test.TestCase):
res_dict = json.loads(res.body)
self.assertEqual(res_dict['server']['status'], 'SHUTOFF')
- def test_create_image_v1_1(self):
- body = {
- 'createImage': {
- 'name': 'Snapshot 1',
- },
- }
- req = webob.Request.blank('/v1.1/servers/1/action')
- req.method = 'POST'
- req.body = json.dumps(body)
- req.headers["content-type"] = "application/json"
- response = req.get_response(fakes.wsgi_app())
- self.assertEqual(202, response.status_int)
- location = response.headers['Location']
- self.assertEqual('http://localhost/v1.1/images/123', location)
-
- def test_create_image_v1_1_with_metadata(self):
- body = {
- 'createImage': {
- 'name': 'Snapshot 1',
- 'metadata': {'key': 'asdf'},
- },
- }
- req = webob.Request.blank('/v1.1/servers/1/action')
- req.method = 'POST'
- req.body = json.dumps(body)
- req.headers["content-type"] = "application/json"
- response = req.get_response(fakes.wsgi_app())
- self.assertEqual(202, response.status_int)
- location = response.headers['Location']
- self.assertEqual('http://localhost/v1.1/images/123', location)
-
- def test_create_image_v1_1_no_name(self):
- body = {
- 'createImage': {},
- }
- req = webob.Request.blank('/v1.1/servers/1/action')
- req.method = 'POST'
- req.body = json.dumps(body)
- req.headers["content-type"] = "application/json"
- response = req.get_response(fakes.wsgi_app())
- self.assertEqual(400, response.status_int)
-
- def test_create_image_v1_1_bad_metadata(self):
- body = {
- 'createImage': {
- 'name': 'geoff',
- 'metadata': 'henry',
- },
- }
- req = webob.Request.blank('/v1.1/servers/1/action')
- req.method = 'POST'
- req.body = json.dumps(body)
- req.headers["content-type"] = "application/json"
- response = req.get_response(fakes.wsgi_app())
- self.assertEqual(400, response.status_int)
-
- def test_create_backup(self):
- """The happy path for creating backups"""
- FLAGS.allow_admin_api = True
-
- body = {
- 'createBackup': {
- 'name': 'Backup 1',
- 'backup_type': 'daily',
- 'rotation': 1,
- },
- }
-
- req = webob.Request.blank('/v1.0/servers/1/action')
- req.method = 'POST'
- req.body = json.dumps(body)
- req.headers["content-type"] = "application/json"
- response = req.get_response(fakes.wsgi_app())
- self.assertEqual(202, response.status_int)
- self.assertTrue(response.headers['Location'])
-
- def test_create_backup_v1_1(self):
- """The happy path for creating backups through v1.1 api"""
- FLAGS.allow_admin_api = True
-
- body = {
- 'createBackup': {
- 'name': 'Backup 1',
- 'backup_type': 'daily',
- 'rotation': 1,
- },
- }
-
- req = webob.Request.blank('/v1.1/servers/1/action')
- req.method = 'POST'
- req.body = json.dumps(body)
- req.headers["content-type"] = "application/json"
- response = req.get_response(fakes.wsgi_app())
- self.assertEqual(202, response.status_int)
- self.assertTrue(response.headers['Location'])
-
- def test_create_backup_admin_api_off(self):
- """The happy path for creating backups"""
- FLAGS.allow_admin_api = False
-
- body = {
- 'createBackup': {
- 'name': 'Backup 1',
- 'backup_type': 'daily',
- 'rotation': 1,
- },
- }
-
- req = webob.Request.blank('/v1.0/servers/1/action')
- req.method = 'POST'
- req.body = json.dumps(body)
- req.headers["content-type"] = "application/json"
- response = req.get_response(fakes.wsgi_app())
- self.assertEqual(501, response.status_int)
-
- def test_create_backup_with_metadata(self):
- FLAGS.allow_admin_api = True
-
- body = {
- 'createBackup': {
- 'name': 'Backup 1',
- 'backup_type': 'daily',
- 'rotation': 1,
- 'metadata': {'123': 'asdf'},
- },
- }
-
- req = webob.Request.blank('/v1.0/servers/1/action')
- req.method = 'POST'
- req.body = json.dumps(body)
- req.headers["content-type"] = "application/json"
- response = req.get_response(fakes.wsgi_app())
- self.assertEqual(202, response.status_int)
- self.assertTrue(response.headers['Location'])
-
- def test_create_backup_no_name(self):
- """Name is required for backups"""
- FLAGS.allow_admin_api = True
-
- body = {
- 'createBackup': {
- 'backup_type': 'daily',
- 'rotation': 1,
- },
- }
-
- req = webob.Request.blank('/v1.0/images')
- req.method = 'POST'
- req.body = json.dumps(body)
- req.headers["content-type"] = "application/json"
- response = req.get_response(fakes.wsgi_app())
- self.assertEqual(400, response.status_int)
-
- def test_create_backup_no_rotation(self):
- """Rotation is required for backup requests"""
- FLAGS.allow_admin_api = True
-
- body = {
- 'createBackup': {
- 'name': 'Backup 1',
- 'backup_type': 'daily',
- },
- }
-
- req = webob.Request.blank('/v1.0/images')
- req.method = 'POST'
- req.body = json.dumps(body)
- req.headers["content-type"] = "application/json"
-
- response = req.get_response(fakes.wsgi_app())
- self.assertEqual(400, response.status_int)
-
- def test_create_backup_no_backup_type(self):
- """Backup Type (daily or weekly) is required for backup requests"""
- FLAGS.allow_admin_api = True
-
- body = {
- 'createBackup': {
- 'name': 'Backup 1',
- 'rotation': 1,
- },
- }
- req = webob.Request.blank('/v1.0/images')
- req.method = 'POST'
- req.body = json.dumps(body)
- req.headers["content-type"] = "application/json"
-
- response = req.get_response(fakes.wsgi_app())
- self.assertEqual(400, response.status_int)
-
- def test_create_backup_bad_entity(self):
- FLAGS.allow_admin_api = True
-
- body = {'createBackup': 'go'}
- req = webob.Request.blank('/v1.0/images')
- req.method = 'POST'
- req.body = json.dumps(body)
- req.headers["content-type"] = "application/json"
-
- response = req.get_response(fakes.wsgi_app())
- self.assertEqual(400, response.status_int)
-
-
-class TestServerActionXMLDeserializer(test.TestCase):
-
- def setUp(self):
- self.deserializer = create_instance_helper.ServerXMLDeserializer()
-
- def tearDown(self):
- pass
-
- def test_create_image(self):
- serial_request = """
-<createImage xmlns="http://docs.openstack.org/compute/api/v1.1"
- name="new-server-test"/>"""
- request = self.deserializer.deserialize(serial_request, 'action')
- expected = {
- "createImage": {
- "name": "new-server-test",
- "metadata": {},
- },
- }
- self.assertEquals(request['body'], expected)
-
- def test_create_image_with_metadata(self):
- serial_request = """
-<createImage xmlns="http://docs.openstack.org/compute/api/v1.1"
- name="new-server-test">
- <metadata>
- <meta key="key1">value1</meta>
- </metadata>
-</createImage>"""
- request = self.deserializer.deserialize(serial_request, 'action')
- expected = {
- "createImage": {
- "name": "new-server-test",
- "metadata": {"key1": "value1"},
- },
- }
- self.assertEquals(request['body'], expected)
-
- def test_create_backup_with_metadata(self):
- serial_request = """
-<createBackup xmlns="http://docs.openstack.org/compute/api/v1.1"
- name="new-server-test"
- rotation="12"
- backup_type="daily">
- <metadata>
- <meta key="key1">value1</meta>
- </metadata>
-</createBackup>"""
- request = self.deserializer.deserialize(serial_request, 'action')
- expected = {
- "createBackup": {
- "name": "new-server-test",
- "rotation": "12",
- "backup_type": "daily",
- "metadata": {"key1": "value1"},
- },
- }
- self.assertEquals(request['body'], expected)
-
class TestServerCreateRequestXMLDeserializerV10(unittest.TestCase):
@@ -2915,10 +2446,11 @@ b25zLiINCg0KLVJpY2hhcmQgQmFjaA==""",
self.assertEqual(request['body'], expected)
-class TestServerCreateRequestXMLDeserializerV11(unittest.TestCase):
+class TestServerCreateRequestXMLDeserializerV11(test.TestCase):
def setUp(self):
- self.deserializer = create_instance_helper.ServerXMLDeserializer()
+ super(TestServerCreateRequestXMLDeserializerV11, self).setUp()
+ self.deserializer = create_instance_helper.ServerXMLDeserializerV11()
def test_minimal_request(self):
serial_request = """
@@ -2932,8 +2464,6 @@ class TestServerCreateRequestXMLDeserializerV11(unittest.TestCase):
"name": "new-server-test",
"imageRef": "1",
"flavorRef": "2",
- "metadata": {},
- "personality": [],
},
}
self.assertEquals(request['body'], expected)
@@ -2952,8 +2482,6 @@ class TestServerCreateRequestXMLDeserializerV11(unittest.TestCase):
"imageRef": "1",
"flavorRef": "2",
"adminPass": "1234",
- "metadata": {},
- "personality": [],
},
}
self.assertEquals(request['body'], expected)
@@ -2970,8 +2498,6 @@ class TestServerCreateRequestXMLDeserializerV11(unittest.TestCase):
"name": "new-server-test",
"imageRef": "http://localhost:8774/v1.1/images/2",
"flavorRef": "3",
- "metadata": {},
- "personality": [],
},
}
self.assertEquals(request['body'], expected)
@@ -2988,8 +2514,6 @@ class TestServerCreateRequestXMLDeserializerV11(unittest.TestCase):
"name": "new-server-test",
"imageRef": "1",
"flavorRef": "http://localhost:8774/v1.1/flavors/3",
- "metadata": {},
- "personality": [],
},
}
self.assertEquals(request['body'], expected)
@@ -3033,7 +2557,6 @@ class TestServerCreateRequestXMLDeserializerV11(unittest.TestCase):
"imageRef": "1",
"flavorRef": "2",
"metadata": {"one": "two", "open": "snack"},
- "personality": [],
},
}
self.assertEquals(request['body'], expected)
@@ -3055,14 +2578,13 @@ class TestServerCreateRequestXMLDeserializerV11(unittest.TestCase):
"name": "new-server-test",
"imageRef": "1",
"flavorRef": "2",
- "metadata": {},
"personality": [
{"path": "/etc/banner.txt", "contents": "MQ=="},
{"path": "/etc/hosts", "contents": "Mg=="},
],
},
}
- self.assertEquals(request['body'], expected)
+ self.assertDictMatch(request['body'], expected)
def test_spec_request(self):
image_bookmark_link = "http://servers.api.openstack.org/1234/" + \
@@ -3264,13 +2786,13 @@ class TestServerInstanceCreation(test.TestCase):
def test_create_instance_with_no_personality(self):
request, response, injected_files = \
self._create_instance_with_personality_json(personality=None)
- self.assertEquals(response.status_int, 200)
+ self.assertEquals(response.status_int, 202)
self.assertEquals(injected_files, [])
def test_create_instance_with_no_personality_xml(self):
request, response, injected_files = \
self._create_instance_with_personality_xml(personality=None)
- self.assertEquals(response.status_int, 200)
+ self.assertEquals(response.status_int, 202)
self.assertEquals(injected_files, [])
def test_create_instance_with_personality(self):
@@ -3280,7 +2802,7 @@ class TestServerInstanceCreation(test.TestCase):
personality = [(path, b64contents)]
request, response, injected_files = \
self._create_instance_with_personality_json(personality)
- self.assertEquals(response.status_int, 200)
+ self.assertEquals(response.status_int, 202)
self.assertEquals(injected_files, [(path, contents)])
def test_create_instance_with_personality_xml(self):
@@ -3290,7 +2812,7 @@ class TestServerInstanceCreation(test.TestCase):
personality = [(path, b64contents)]
request, response, injected_files = \
self._create_instance_with_personality_xml(personality)
- self.assertEquals(response.status_int, 200)
+ self.assertEquals(response.status_int, 202)
self.assertEquals(injected_files, [(path, contents)])
def test_create_instance_with_personality_no_path(self):
@@ -3353,7 +2875,7 @@ class TestServerInstanceCreation(test.TestCase):
request = self._get_create_request_json(body_dict)
compute_api, response = \
self._run_create_instance_with_mock_compute_api(request)
- self.assertEquals(response.status_int, 200)
+ self.assertEquals(response.status_int, 202)
def test_create_instance_with_three_personalities(self):
files = [
@@ -3366,7 +2888,7 @@ class TestServerInstanceCreation(test.TestCase):
personality.append((path, base64.b64encode(content)))
request, response, injected_files = \
self._create_instance_with_personality_json(personality)
- self.assertEquals(response.status_int, 200)
+ self.assertEquals(response.status_int, 202)
self.assertEquals(injected_files, files)
def test_create_instance_personality_empty_content(self):
@@ -3375,13 +2897,13 @@ class TestServerInstanceCreation(test.TestCase):
personality = [(path, contents)]
request, response, injected_files = \
self._create_instance_with_personality_json(personality)
- self.assertEquals(response.status_int, 200)
+ self.assertEquals(response.status_int, 202)
self.assertEquals(injected_files, [(path, contents)])
def test_create_instance_admin_pass_json(self):
request, response, dummy = \
self._create_instance_with_personality_json(None)
- self.assertEquals(response.status_int, 200)
+ self.assertEquals(response.status_int, 202)
response = json.loads(response.body)
self.assertTrue('adminPass' in response['server'])
self.assertEqual(16, len(response['server']['adminPass']))
@@ -3389,7 +2911,7 @@ class TestServerInstanceCreation(test.TestCase):
def test_create_instance_admin_pass_xml(self):
request, response, dummy = \
self._create_instance_with_personality_xml(None)
- self.assertEquals(response.status_int, 200)
+ self.assertEquals(response.status_int, 202)
dom = minidom.parseString(response.body)
server = dom.childNodes[0]
self.assertEquals(server.nodeName, 'server')
diff --git a/nova/tests/api/openstack/test_users.py b/nova/tests/api/openstack/test_users.py
index 705c02f6b..1d133f9ab 100644
--- a/nova/tests/api/openstack/test_users.py
+++ b/nova/tests/api/openstack/test_users.py
@@ -17,7 +17,6 @@ import json
import webob
-from nova import flags
from nova import test
from nova import utils
from nova.api.openstack import users
@@ -25,10 +24,6 @@ from nova.auth.manager import User, Project
from nova.tests.api.openstack import fakes
-FLAGS = flags.FLAGS
-FLAGS.verbose = True
-
-
def fake_init(self):
self.manager = fakes.FakeAuthManager()
@@ -40,7 +35,7 @@ def fake_admin_check(self, req):
class UsersTest(test.TestCase):
def setUp(self):
super(UsersTest, self).setUp()
- self.flags(allow_admin_api=True)
+ self.flags(verbose=True, allow_admin_api=True)
self.stubs.Set(users.Controller, '__init__',
fake_init)
self.stubs.Set(users.Controller, '_check_admin',
@@ -56,7 +51,6 @@ class UsersTest(test.TestCase):
fakes.stub_out_rate_limiting(self.stubs)
fakes.stub_out_auth(self.stubs)
- self.allow_admin = FLAGS.allow_admin_api
fakemgr = fakes.FakeAuthManager()
fakemgr.add_user(User('id1', 'guy1', 'acc1', 'secret1', False))
fakemgr.add_user(User('id2', 'guy2', 'acc2', 'secret2', True))
diff --git a/nova/tests/api/openstack/test_versions.py b/nova/tests/api/openstack/test_versions.py
index e68455778..1269f13c9 100644
--- a/nova/tests/api/openstack/test_versions.py
+++ b/nova/tests/api/openstack/test_versions.py
@@ -38,24 +38,24 @@ VERSIONS = {
"rel": "describedby",
"type": "application/pdf",
"href": "http://docs.rackspacecloud.com/"
- "servers/api/v1.0/cs-devguide-20110125.pdf"
+ "servers/api/v1.0/cs-devguide-20110125.pdf",
},
{
"rel": "describedby",
"type": "application/vnd.sun.wadl+xml",
"href": "http://docs.rackspacecloud.com/"
- "servers/api/v1.0/application.wadl"
+ "servers/api/v1.0/application.wadl",
},
],
"media-types": [
{
"base": "application/xml",
- "type": "application/vnd.openstack.compute-v1.0+xml"
+ "type": "application/vnd.openstack.compute-v1.0+xml",
},
{
"base": "application/json",
- "type": "application/vnd.openstack.compute-v1.0+json"
- }
+ "type": "application/vnd.openstack.compute-v1.0+json",
+ },
],
},
"v1.1": {
@@ -67,24 +67,24 @@ VERSIONS = {
"rel": "describedby",
"type": "application/pdf",
"href": "http://docs.rackspacecloud.com/"
- "servers/api/v1.1/cs-devguide-20110125.pdf"
+ "servers/api/v1.1/cs-devguide-20110125.pdf",
},
{
"rel": "describedby",
"type": "application/vnd.sun.wadl+xml",
"href": "http://docs.rackspacecloud.com/"
- "servers/api/v1.1/application.wadl"
+ "servers/api/v1.1/application.wadl",
},
],
"media-types": [
{
"base": "application/xml",
- "type": "application/vnd.openstack.compute-v1.1+xml"
+ "type": "application/vnd.openstack.compute-v1.1+xml",
},
{
"base": "application/json",
- "type": "application/vnd.openstack.compute-v1.1+json"
- }
+ "type": "application/vnd.openstack.compute-v1.1+json",
+ },
],
},
}
@@ -150,34 +150,34 @@ class VersionsTest(test.TestCase):
"links": [
{
"rel": "self",
- "href": "http://localhost/v1.0/"
+ "href": "http://localhost/v1.0/",
},
{
"rel": "describedby",
"type": "application/pdf",
"href": "http://docs.rackspacecloud.com/"
- "servers/api/v1.0/cs-devguide-20110125.pdf"
+ "servers/api/v1.0/cs-devguide-20110125.pdf",
},
{
"rel": "describedby",
"type": "application/vnd.sun.wadl+xml",
"href": "http://docs.rackspacecloud.com/"
- "servers/api/v1.0/application.wadl"
- }
+ "servers/api/v1.0/application.wadl",
+ },
],
"media-types": [
{
"base": "application/xml",
"type": "application/"
- "vnd.openstack.compute-v1.0+xml"
+ "vnd.openstack.compute-v1.0+xml",
},
{
"base": "application/json",
"type": "application/"
- "vnd.openstack.compute-v1.0+json"
- }
- ]
- }
+ "vnd.openstack.compute-v1.0+json",
+ },
+ ],
+ },
}
self.assertEqual(expected, version)
@@ -196,34 +196,34 @@ class VersionsTest(test.TestCase):
"links": [
{
"rel": "self",
- "href": "http://localhost/v1.1/"
+ "href": "http://localhost/v1.1/",
},
{
"rel": "describedby",
"type": "application/pdf",
"href": "http://docs.rackspacecloud.com/"
- "servers/api/v1.1/cs-devguide-20110125.pdf"
+ "servers/api/v1.1/cs-devguide-20110125.pdf",
},
{
"rel": "describedby",
"type": "application/vnd.sun.wadl+xml",
"href": "http://docs.rackspacecloud.com/"
- "servers/api/v1.1/application.wadl"
- }
+ "servers/api/v1.1/application.wadl",
+ },
],
"media-types": [
{
"base": "application/xml",
"type": "application/"
- "vnd.openstack.compute-v1.1+xml"
+ "vnd.openstack.compute-v1.1+xml",
},
{
"base": "application/json",
"type": "application/"
- "vnd.openstack.compute-v1.1+json"
- }
- ]
- }
+ "vnd.openstack.compute-v1.1+json",
+ },
+ ],
+ },
}
self.assertEqual(expected, version)
@@ -739,30 +739,30 @@ class VersionsSerializerTests(test.TestCase):
"links": [
{
"rel": "self",
- "href": "http://localhost/v1.0/"
+ "href": "http://localhost/v1.0/",
},
{
"rel": "describedby",
"type": "application/pdf",
"href": "http://docs.rackspacecloud.com/"
- "servers/api/v1.0/cs-devguide-20110125.pdf"
+ "servers/api/v1.0/cs-devguide-20110125.pdf",
},
{
"rel": "describedby",
"type": "application/vnd.sun.wadl+xml",
"href": "http://docs.rackspacecloud.com/"
- "servers/api/v1.0/application.wadl"
+ "servers/api/v1.0/application.wadl",
},
],
"media-types": [
{
"base": "application/xml",
- "type": "application/vnd.openstack.compute-v1.0+xml"
+ "type": "application/vnd.openstack.compute-v1.0+xml",
},
{
"base": "application/json",
- "type": "application/vnd.openstack.compute-v1.0+json"
- }
+ "type": "application/vnd.openstack.compute-v1.0+json",
+ },
],
},
}
@@ -874,29 +874,29 @@ class VersionsSerializerTests(test.TestCase):
"links": [
{
"rel": "self",
- "href": "http://localhost/v1.1/"
+ "href": "http://localhost/v1.1/",
},
{
"rel": "describedby",
"type": "application/pdf",
"href": "http://docs.rackspacecloud.com/"
- "servers/api/v1.1/cs-devguide-20110125.pdf"
+ "servers/api/v1.1/cs-devguide-20110125.pdf",
},
{
"rel": "describedby",
"type": "application/vnd.sun.wadl+xml",
"href": "http://docs.rackspacecloud.com/"
- "servers/api/v1.1/application.wadl"
+ "servers/api/v1.1/application.wadl",
},
],
"media-types": [
{
"base": "application/xml",
- "type": "application/vnd.openstack.compute-v1.1+xml"
+ "type": "application/vnd.openstack.compute-v1.1+xml",
},
{
"base": "application/json",
- "type": "application/vnd.openstack.compute-v1.1+json"
+ "type": "application/vnd.openstack.compute-v1.1+json",
}
],
},
diff --git a/nova/tests/api/openstack/test_zones.py b/nova/tests/api/openstack/test_zones.py
index 3deb844aa..4a46a5764 100644
--- a/nova/tests/api/openstack/test_zones.py
+++ b/nova/tests/api/openstack/test_zones.py
@@ -29,7 +29,6 @@ from nova.scheduler import api
FLAGS = flags.FLAGS
-FLAGS.verbose = True
def zone_get(context, zone_id):
@@ -95,7 +94,7 @@ def zone_select(context, specs):
class ZonesTest(test.TestCase):
def setUp(self):
super(ZonesTest, self).setUp()
- self.flags(allow_admin_api=True)
+ self.flags(verbose=True, allow_admin_api=True)
fakes.stub_out_networking(self.stubs)
fakes.stub_out_rate_limiting(self.stubs)
diff --git a/nova/tests/fake_utils.py b/nova/tests/fake_utils.py
index be59970c9..84ab641ea 100644
--- a/nova/tests/fake_utils.py
+++ b/nova/tests/fake_utils.py
@@ -64,8 +64,10 @@ def fake_execute(*cmd_parts, **kwargs):
global _fake_execute_repliers
process_input = kwargs.get('process_input', None)
- addl_env = kwargs.get('addl_env', None)
check_exit_code = kwargs.get('check_exit_code', 0)
+ delay_on_retry = kwargs.get('delay_on_retry', True)
+ attempts = kwargs.get('attempts', 1)
+ run_as_root = kwargs.get('run_as_root', False)
cmd_str = ' '.join(str(part) for part in cmd_parts)
LOG.debug(_("Faking execution of cmd (subprocess): %s"), cmd_str)
@@ -87,7 +89,9 @@ def fake_execute(*cmd_parts, **kwargs):
# Alternative is a function, so call it
reply = reply_handler(cmd_parts,
process_input=process_input,
- addl_env=addl_env,
+ delay_on_retry=delay_on_retry,
+ attempts=attempts,
+ run_as_root=run_as_root,
check_exit_code=check_exit_code)
except exception.ProcessExecutionError as e:
LOG.debug(_('Faked command raised an exception %s' % str(e)))
diff --git a/nova/tests/hyperv_unittest.py b/nova/tests/hyperv_unittest.py
index 0ea196950..d346d0a70 100644
--- a/nova/tests/hyperv_unittest.py
+++ b/nova/tests/hyperv_unittest.py
@@ -21,13 +21,9 @@ import random
from nova import context
from nova import db
-from nova import flags
from nova import test
from nova.virt import hyperv
-FLAGS = flags.FLAGS
-FLAGS.connection_type = 'hyperv'
-
class HyperVTestCase(test.TestCase):
"""Test cases for the Hyper-V driver"""
@@ -36,6 +32,7 @@ class HyperVTestCase(test.TestCase):
self.user_id = 'fake'
self.project_id = 'fake'
self.context = context.RequestContext(self.user_id, self.project_id)
+ self.flags(connection_type='hyperv')
def test_create_destroy(self):
"""Create a VM and destroy it"""
diff --git a/nova/tests/image/test_glance.py b/nova/tests/image/test_glance.py
index 5a40f578f..0ff508ffa 100644
--- a/nova/tests/image/test_glance.py
+++ b/nova/tests/image/test_glance.py
@@ -235,3 +235,39 @@ class TestMutatorDateTimeTests(BaseGlanceTest):
'updated_at': None,
'deleted_at': None}
return fixture
+
+
+class TestGlanceSerializer(unittest.TestCase):
+ def test_serialize(self):
+ metadata = {'name': 'image1',
+ 'is_public': True,
+ 'foo': 'bar',
+ 'properties': {
+ 'prop1': 'propvalue1',
+ 'mappings': [
+ {'virtual': 'aaa',
+ 'device': 'bbb'},
+ {'virtual': 'xxx',
+ 'device': 'yyy'}],
+ 'block_device_mapping': [
+ {'virtual_device': 'fake',
+ 'device_name': '/dev/fake'},
+ {'virtual_device': 'ephemeral0',
+ 'device_name': '/dev/fake0'}]}}
+
+ converted_expected = {
+ 'name': 'image1',
+ 'is_public': True,
+ 'foo': 'bar',
+ 'properties': {
+ 'prop1': 'propvalue1',
+ 'mappings':
+ '[{"device": "bbb", "virtual": "aaa"}, '
+ '{"device": "yyy", "virtual": "xxx"}]',
+ 'block_device_mapping':
+ '[{"virtual_device": "fake", "device_name": "/dev/fake"}, '
+ '{"virtual_device": "ephemeral0", '
+ '"device_name": "/dev/fake0"}]'}}
+ converted = glance._convert_to_string(metadata)
+ self.assertEqual(converted, converted_expected)
+ self.assertEqual(glance._convert_from_string(converted), metadata)
diff --git a/nova/tests/image/test_s3.py b/nova/tests/image/test_s3.py
index 231e109f8..f1ceeb7fe 100644
--- a/nova/tests/image/test_s3.py
+++ b/nova/tests/image/test_s3.py
@@ -16,12 +16,9 @@
# under the License.
from nova import context
-from nova import flags
from nova import test
from nova.image import s3
-FLAGS = flags.FLAGS
-
ami_manifest_xml = """<?xml version="1.0" ?>
<manifest>
@@ -59,15 +56,10 @@ ami_manifest_xml = """<?xml version="1.0" ?>
class TestS3ImageService(test.TestCase):
def setUp(self):
super(TestS3ImageService, self).setUp()
- self.orig_image_service = FLAGS.image_service
- FLAGS.image_service = 'nova.image.fake.FakeImageService'
+ self.flags(image_service='nova.image.fake.FakeImageService')
self.image_service = s3.S3ImageService()
self.context = context.RequestContext(None, None)
- def tearDown(self):
- super(TestS3ImageService, self).tearDown()
- FLAGS.image_service = self.orig_image_service
-
def _assertEqualList(self, list0, list1, keys):
self.assertEqual(len(list0), len(list1))
key = keys[0]
diff --git a/nova/tests/integrated/integrated_helpers.py b/nova/tests/integrated/integrated_helpers.py
index 47bd8c1e4..fb2f88502 100644
--- a/nova/tests/integrated/integrated_helpers.py
+++ b/nova/tests/integrated/integrated_helpers.py
@@ -23,7 +23,6 @@ import random
import string
from nova import exception
-from nova import flags
from nova import service
from nova import test # For the flags
from nova.auth import manager
@@ -32,8 +31,6 @@ from nova.log import logging
from nova.tests.integrated.api import client
-FLAGS = flags.FLAGS
-
LOG = logging.getLogger('nova.tests.integrated')
@@ -151,6 +148,7 @@ class _IntegratedTestBase(test.TestCase):
f = self._get_flags()
self.flags(**f)
+ self.flags(verbose=True)
def fake_get_image_service(image_href):
image_id = int(str(image_href).split('/')[-1])
diff --git a/nova/tests/integrated/test_extensions.py b/nova/tests/integrated/test_extensions.py
index 0d4ee8cab..c22cf0be0 100644
--- a/nova/tests/integrated/test_extensions.py
+++ b/nova/tests/integrated/test_extensions.py
@@ -17,7 +17,6 @@
import os
-from nova import flags
from nova.log import logging
from nova.tests.integrated import integrated_helpers
@@ -25,10 +24,6 @@ from nova.tests.integrated import integrated_helpers
LOG = logging.getLogger('nova.tests.integrated')
-FLAGS = flags.FLAGS
-FLAGS.verbose = True
-
-
class ExtensionsTest(integrated_helpers._IntegratedTestBase):
def _get_flags(self):
f = super(ExtensionsTest, self)._get_flags()
diff --git a/nova/tests/integrated/test_login.py b/nova/tests/integrated/test_login.py
index a5180b6bc..06359a52f 100644
--- a/nova/tests/integrated/test_login.py
+++ b/nova/tests/integrated/test_login.py
@@ -17,7 +17,6 @@
import unittest
-from nova import flags
from nova.log import logging
from nova.tests.integrated import integrated_helpers
from nova.tests.integrated.api import client
@@ -25,9 +24,6 @@ from nova.tests.integrated.api import client
LOG = logging.getLogger('nova.tests.integrated')
-FLAGS = flags.FLAGS
-FLAGS.verbose = True
-
class LoginTest(integrated_helpers._IntegratedTestBase):
def test_login(self):
diff --git a/nova/tests/integrated/test_servers.py b/nova/tests/integrated/test_servers.py
index 67b3c485a..150279a95 100644
--- a/nova/tests/integrated/test_servers.py
+++ b/nova/tests/integrated/test_servers.py
@@ -18,7 +18,6 @@
import time
import unittest
-from nova import flags
from nova.log import logging
from nova.tests.integrated import integrated_helpers
from nova.tests.integrated.api import client
@@ -27,10 +26,6 @@ from nova.tests.integrated.api import client
LOG = logging.getLogger('nova.tests.integrated')
-FLAGS = flags.FLAGS
-FLAGS.verbose = True
-
-
class ServersTest(integrated_helpers._IntegratedTestBase):
def test_get_servers(self):
"""Simple check that listing servers works."""
diff --git a/nova/tests/integrated/test_volumes.py b/nova/tests/integrated/test_volumes.py
index e9fb3c4d1..d3e936462 100644
--- a/nova/tests/integrated/test_volumes.py
+++ b/nova/tests/integrated/test_volumes.py
@@ -18,7 +18,6 @@
import unittest
import time
-from nova import flags
from nova.log import logging
from nova.tests.integrated import integrated_helpers
from nova.tests.integrated.api import client
@@ -28,10 +27,6 @@ from nova.volume import driver
LOG = logging.getLogger('nova.tests.integrated')
-FLAGS = flags.FLAGS
-FLAGS.verbose = True
-
-
class VolumesTest(integrated_helpers._IntegratedTestBase):
def setUp(self):
super(VolumesTest, self).setUp()
diff --git a/nova/tests/integrated/test_xml.py b/nova/tests/integrated/test_xml.py
index fde32f797..74baaacc2 100644
--- a/nova/tests/integrated/test_xml.py
+++ b/nova/tests/integrated/test_xml.py
@@ -15,7 +15,6 @@
# License for the specific language governing permissions and limitations
# under the License.
-from nova import flags
from nova.log import logging
from nova.tests.integrated import integrated_helpers
from nova.api.openstack import common
@@ -24,10 +23,6 @@ from nova.api.openstack import common
LOG = logging.getLogger('nova.tests.integrated')
-FLAGS = flags.FLAGS
-FLAGS.verbose = True
-
-
class XmlTests(integrated_helpers._IntegratedTestBase):
""""Some basic XML sanity checks."""
diff --git a/nova/tests/scheduler/test_host_filter.py b/nova/tests/scheduler/test_host_filter.py
index b1892dab4..7e664d3f9 100644
--- a/nova/tests/scheduler/test_host_filter.py
+++ b/nova/tests/scheduler/test_host_filter.py
@@ -19,12 +19,9 @@ Tests For Scheduler Host Filters.
import json
from nova import exception
-from nova import flags
from nova import test
from nova.scheduler import host_filter
-FLAGS = flags.FLAGS
-
class FakeZoneManager:
pass
@@ -57,9 +54,9 @@ class HostFilterTestCase(test.TestCase):
'host_name-label': 'xs-%s' % multiplier}
def setUp(self):
- self.old_flag = FLAGS.default_host_filter
- FLAGS.default_host_filter = \
- 'nova.scheduler.host_filter.AllHostsFilter'
+ super(HostFilterTestCase, self).setUp()
+ default_host_filter = 'nova.scheduler.host_filter.AllHostsFilter'
+ self.flags(default_host_filter=default_host_filter)
self.instance_type = dict(name='tiny',
memory_mb=50,
vcpus=10,
@@ -98,9 +95,6 @@ class HostFilterTestCase(test.TestCase):
host09['xpu_arch'] = 'fermi'
host09['xpu_info'] = 'Tesla 2150'
- def tearDown(self):
- FLAGS.default_host_filter = self.old_flag
-
def test_choose_filter(self):
# Test default filter ...
hf = host_filter.choose_host_filter()
diff --git a/nova/tests/scheduler/test_least_cost_scheduler.py b/nova/tests/scheduler/test_least_cost_scheduler.py
index 49791053e..fbe6b2f77 100644
--- a/nova/tests/scheduler/test_least_cost_scheduler.py
+++ b/nova/tests/scheduler/test_least_cost_scheduler.py
@@ -16,13 +16,11 @@
Tests For Least Cost Scheduler
"""
-from nova import flags
from nova import test
from nova.scheduler import least_cost
from nova.tests.scheduler import test_zone_aware_scheduler
MB = 1024 * 1024
-FLAGS = flags.FLAGS
class FakeHost(object):
@@ -95,10 +93,9 @@ class LeastCostSchedulerTestCase(test.TestCase):
self.assertWeights(expected, num, request_spec, hosts)
def test_noop_cost_fn(self):
- FLAGS.least_cost_scheduler_cost_functions = [
- 'nova.scheduler.least_cost.noop_cost_fn',
- ]
- FLAGS.noop_cost_fn_weight = 1
+ self.flags(least_cost_scheduler_cost_functions=[
+ 'nova.scheduler.least_cost.noop_cost_fn'],
+ noop_cost_fn_weight=1)
num = 1
request_spec = {}
@@ -109,10 +106,9 @@ class LeastCostSchedulerTestCase(test.TestCase):
self.assertWeights(expected, num, request_spec, hosts)
def test_cost_fn_weights(self):
- FLAGS.least_cost_scheduler_cost_functions = [
- 'nova.scheduler.least_cost.noop_cost_fn',
- ]
- FLAGS.noop_cost_fn_weight = 2
+ self.flags(least_cost_scheduler_cost_functions=[
+ 'nova.scheduler.least_cost.noop_cost_fn'],
+ noop_cost_fn_weight=2)
num = 1
request_spec = {}
@@ -123,10 +119,9 @@ class LeastCostSchedulerTestCase(test.TestCase):
self.assertWeights(expected, num, request_spec, hosts)
def test_compute_fill_first_cost_fn(self):
- FLAGS.least_cost_scheduler_cost_functions = [
- 'nova.scheduler.least_cost.compute_fill_first_cost_fn',
- ]
- FLAGS.compute_fill_first_cost_fn_weight = 1
+ self.flags(least_cost_scheduler_cost_functions=[
+ 'nova.scheduler.least_cost.compute_fill_first_cost_fn'],
+ compute_fill_first_cost_fn_weight=1)
num = 1
instance_type = {'memory_mb': 1024}
diff --git a/nova/tests/scheduler/test_scheduler.py b/nova/tests/scheduler/test_scheduler.py
index 6a56a57db..7a26fd1bb 100644
--- a/nova/tests/scheduler/test_scheduler.py
+++ b/nova/tests/scheduler/test_scheduler.py
@@ -21,9 +21,11 @@ Tests For Scheduler
import datetime
import mox
-import novaclient.exceptions
import stubout
+from novaclient import v1_1 as novaclient
+from novaclient import exceptions as novaclient_exceptions
+
from mox import IgnoreArg
from nova import context
from nova import db
@@ -301,7 +303,7 @@ class SimpleDriverTestCase(test.TestCase):
db.compute_node_create(self.context, dic)
return db.service_get(self.context, s_ref['id'])
- def test_doesnt_report_disabled_hosts_as_up(self):
+ def test_doesnt_report_disabled_hosts_as_up_no_queue(self):
"""Ensures driver doesn't find hosts before they are enabled"""
# NOTE(vish): constructing service without create method
# because we are going to use it without queue
@@ -324,7 +326,7 @@ class SimpleDriverTestCase(test.TestCase):
compute1.kill()
compute2.kill()
- def test_reports_enabled_hosts_as_up(self):
+ def test_reports_enabled_hosts_as_up_no_queue(self):
"""Ensures driver can find the hosts that are up"""
# NOTE(vish): constructing service without create method
# because we are going to use it without queue
@@ -343,7 +345,7 @@ class SimpleDriverTestCase(test.TestCase):
compute1.kill()
compute2.kill()
- def test_least_busy_host_gets_instance(self):
+ def test_least_busy_host_gets_instance_no_queue(self):
"""Ensures the host with less cores gets the next one"""
compute1 = service.Service('host1',
'nova-compute',
@@ -366,7 +368,7 @@ class SimpleDriverTestCase(test.TestCase):
compute1.kill()
compute2.kill()
- def test_specific_host_gets_instance(self):
+ def test_specific_host_gets_instance_no_queue(self):
"""Ensures if you set availability_zone it launches on that zone"""
compute1 = service.Service('host1',
'nova-compute',
@@ -389,7 +391,7 @@ class SimpleDriverTestCase(test.TestCase):
compute1.kill()
compute2.kill()
- def test_wont_sechedule_if_specified_host_is_down(self):
+ def test_wont_sechedule_if_specified_host_is_down_no_queue(self):
compute1 = service.Service('host1',
'nova-compute',
'compute',
@@ -408,7 +410,7 @@ class SimpleDriverTestCase(test.TestCase):
db.instance_destroy(self.context, instance_id2)
compute1.kill()
- def test_will_schedule_on_disabled_host_if_specified(self):
+ def test_will_schedule_on_disabled_host_if_specified_no_queue(self):
compute1 = service.Service('host1',
'nova-compute',
'compute',
@@ -423,7 +425,7 @@ class SimpleDriverTestCase(test.TestCase):
db.instance_destroy(self.context, instance_id2)
compute1.kill()
- def test_too_many_cores(self):
+ def test_too_many_cores_no_queue(self):
"""Ensures we don't go over max cores"""
compute1 = service.Service('host1',
'nova-compute',
@@ -456,7 +458,7 @@ class SimpleDriverTestCase(test.TestCase):
compute1.kill()
compute2.kill()
- def test_least_busy_host_gets_volume(self):
+ def test_least_busy_host_gets_volume_no_queue(self):
"""Ensures the host with less gigabytes gets the next one"""
volume1 = service.Service('host1',
'nova-volume',
@@ -477,7 +479,7 @@ class SimpleDriverTestCase(test.TestCase):
volume1.delete_volume(self.context, volume_id1)
db.volume_destroy(self.context, volume_id2)
- def test_doesnt_report_disabled_hosts_as_up(self):
+ def test_doesnt_report_disabled_hosts_as_up2(self):
"""Ensures driver doesn't find hosts before they are enabled"""
compute1 = self.start_service('compute', host='host1')
compute2 = self.start_service('compute', host='host2')
@@ -962,13 +964,10 @@ class ZoneRedirectTest(test.TestCase):
self.stubs.Set(db, 'zone_get_all', zone_get_all)
self.stubs.Set(db, 'instance_get_by_uuid',
fake_instance_get_by_uuid)
-
- self.enable_zone_routing = FLAGS.enable_zone_routing
- FLAGS.enable_zone_routing = True
+ self.flags(enable_zone_routing=True)
def tearDown(self):
self.stubs.UnsetAll()
- FLAGS.enable_zone_routing = self.enable_zone_routing
super(ZoneRedirectTest, self).tearDown()
def test_trap_found_locally(self):
@@ -993,12 +992,12 @@ class ZoneRedirectTest(test.TestCase):
decorator = FakeRerouteCompute("foo", id_to_return=FAKE_UUID_NOT_FOUND)
try:
result = decorator(go_boom)(None, None, 1)
- self.assertFail(_("Should have rerouted."))
+ self.fail(_("Should have rerouted."))
except api.RedirectResult, e:
self.assertEquals(e.results['magic'], 'found me')
def test_routing_flags(self):
- FLAGS.enable_zone_routing = False
+ self.flags(enable_zone_routing=False)
decorator = FakeRerouteCompute("foo")
self.assertRaises(exception.InstanceNotFound, decorator(go_boom),
None, None, 1)
@@ -1039,10 +1038,10 @@ class FakeServerCollection(object):
class FakeEmptyServerCollection(object):
def get(self, f):
- raise novaclient.NotFound(1)
+ raise novaclient_exceptions.NotFound(1)
def find(self, name):
- raise novaclient.NotFound(2)
+ raise novaclient_exceptions.NotFound(2)
class FakeNovaClient(object):
@@ -1081,14 +1080,14 @@ class DynamicNovaClientTest(test.TestCase):
class FakeZonesProxy(object):
- def do_something(*args, **kwargs):
+ def do_something(self, *args, **kwargs):
return 42
- def raises_exception(*args, **kwargs):
+ def raises_exception(self, *args, **kwargs):
raise Exception('testing')
-class FakeNovaClientOpenStack(object):
+class FakeNovaClientZones(object):
def __init__(self, *args, **kwargs):
self.zones = FakeZonesProxy()
@@ -1101,7 +1100,7 @@ class CallZoneMethodTest(test.TestCase):
super(CallZoneMethodTest, self).setUp()
self.stubs = stubout.StubOutForTesting()
self.stubs.Set(db, 'zone_get_all', zone_get_all)
- self.stubs.Set(novaclient, 'OpenStack', FakeNovaClientOpenStack)
+ self.stubs.Set(novaclient, 'Client', FakeNovaClientZones)
def tearDown(self):
self.stubs.UnsetAll()
diff --git a/nova/tests/scheduler/test_zone_aware_scheduler.py b/nova/tests/scheduler/test_zone_aware_scheduler.py
index 7833028c3..788efca52 100644
--- a/nova/tests/scheduler/test_zone_aware_scheduler.py
+++ b/nova/tests/scheduler/test_zone_aware_scheduler.py
@@ -21,7 +21,9 @@ import json
import nova.db
from nova import exception
+from nova import rpc
from nova import test
+from nova.compute import api as compute_api
from nova.scheduler import driver
from nova.scheduler import zone_aware_scheduler
from nova.scheduler import zone_manager
@@ -114,7 +116,7 @@ def fake_provision_resource_from_blob(context, item, instance_id,
def fake_decrypt_blob_returns_local_info(blob):
- return {'foo': True} # values aren't important.
+ return {'hostname': 'foooooo'} # values aren't important.
def fake_decrypt_blob_returns_child_info(blob):
@@ -283,14 +285,29 @@ class ZoneAwareSchedulerTestCase(test.TestCase):
global was_called
sched = FakeZoneAwareScheduler()
was_called = False
+
+ def fake_create_db_entry_for_new_instance(self, context,
+ image, base_options, security_group,
+ block_device_mapping, num=1):
+ global was_called
+ was_called = True
+ # return fake instances
+ return {'id': 1, 'uuid': 'f874093c-7b17-49c0-89c3-22a5348497f9'}
+
+ def fake_rpc_cast(*args, **kwargs):
+ pass
+
self.stubs.Set(sched, '_decrypt_blob',
fake_decrypt_blob_returns_local_info)
- self.stubs.Set(sched, '_provision_resource_locally',
- fake_provision_resource_locally)
+ self.stubs.Set(compute_api.API,
+ 'create_db_entry_for_new_instance',
+ fake_create_db_entry_for_new_instance)
+ self.stubs.Set(rpc, 'cast', fake_rpc_cast)
- request_spec = {'blob': "Non-None blob data"}
+ build_plan_item = {'blob': "Non-None blob data"}
+ request_spec = {'image': {}, 'instance_properties': {}}
- sched._provision_resource_from_blob(None, request_spec, 1,
+ sched._provision_resource_from_blob(None, build_plan_item, 1,
request_spec, {})
self.assertTrue(was_called)
diff --git a/nova/tests/test_api.py b/nova/tests/test_api.py
index d9b1d39c9..2011ae756 100644
--- a/nova/tests/test_api.py
+++ b/nova/tests/test_api.py
@@ -27,6 +27,7 @@ import random
import StringIO
import webob
+from nova import block_device
from nova import context
from nova import exception
from nova import test
@@ -147,10 +148,12 @@ class Ec2utilsTestCase(test.TestCase):
properties0 = {'mappings': mappings}
properties1 = {'root_device_name': '/dev/sdb', 'mappings': mappings}
- root_device_name = ec2utils.properties_root_device_name(properties0)
+ root_device_name = block_device.properties_root_device_name(
+ properties0)
self.assertEqual(root_device_name, '/dev/sda1')
- root_device_name = ec2utils.properties_root_device_name(properties1)
+ root_device_name = block_device.properties_root_device_name(
+ properties1)
self.assertEqual(root_device_name, '/dev/sdb')
def test_mapping_prepend_dev(self):
@@ -184,7 +187,7 @@ class Ec2utilsTestCase(test.TestCase):
'device': '/dev/sdc1'},
{'virtual': 'ephemeral1',
'device': '/dev/sdc1'}]
- self.assertDictListMatch(ec2utils.mappings_prepend_dev(mappings),
+ self.assertDictListMatch(block_device.mappings_prepend_dev(mappings),
expected_result)
@@ -336,6 +339,33 @@ class ApiEc2TestCase(test.TestCase):
self.ec2.delete_security_group(security_group_name)
+ def test_group_name_valid_chars_security_group(self):
+ """ Test that we sanely handle invalid security group names.
+ API Spec states we should only accept alphanumeric characters,
+ spaces, dashes, and underscores. """
+ self.expect_http()
+ self.mox.ReplayAll()
+
+ # Test block group_name of non alphanumeric characters, spaces,
+ # dashes, and underscores.
+ security_group_name = "aa #^% -=99"
+
+ self.assertRaises(EC2ResponseError, self.ec2.create_security_group,
+ security_group_name, 'test group')
+
+ def test_group_name_valid_length_security_group(self):
+ """Test that we sanely handle invalid security group names.
+ API Spec states that the length should not exceed 255 chars """
+ self.expect_http()
+ self.mox.ReplayAll()
+
+ # Test block group_name > 255 chars
+ security_group_name = "".join(random.choice("poiuytrewqasdfghjklmnbvc")
+ for x in range(random.randint(256, 266)))
+
+ self.assertRaises(EC2ResponseError, self.ec2.create_security_group,
+ security_group_name, 'test group')
+
def test_authorize_revoke_security_group_cidr(self):
"""
Test that we can add and remove CIDR based rules
diff --git a/nova/tests/test_auth.py b/nova/tests/test_auth.py
index 7c0f783bb..4561eb7f2 100644
--- a/nova/tests/test_auth.py
+++ b/nova/tests/test_auth.py
@@ -62,7 +62,12 @@ class project_generator(object):
class user_and_project_generator(object):
- def __init__(self, manager, user_state={}, project_state={}):
+ def __init__(self, manager, user_state=None, project_state=None):
+ if not user_state:
+ user_state = {}
+ if not project_state:
+ project_state = {}
+
self.manager = manager
if 'name' not in user_state:
user_state['name'] = 'test1'
@@ -83,9 +88,9 @@ class user_and_project_generator(object):
class _AuthManagerBaseTestCase(test.TestCase):
def setUp(self):
- FLAGS.auth_driver = self.auth_driver
super(_AuthManagerBaseTestCase, self).setUp()
- self.flags(connection_type='fake')
+ self.flags(auth_driver=self.auth_driver,
+ connection_type='fake')
self.manager = manager.AuthManager(new=True)
self.manager.mc.cache = {}
diff --git a/nova/tests/test_block_device.py b/nova/tests/test_block_device.py
new file mode 100644
index 000000000..b8e9b35e2
--- /dev/null
+++ b/nova/tests/test_block_device.py
@@ -0,0 +1,87 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2011 Isaku Yamahata
+# 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.
+
+"""
+Tests for Block Device utility functions.
+"""
+
+from nova import block_device
+from nova import test
+
+
+class BlockDeviceTestCase(test.TestCase):
+ def test_properties(self):
+ root_device0 = '/dev/sda'
+ root_device1 = '/dev/sdb'
+ mappings = [{'virtual': 'root',
+ 'device': root_device0}]
+
+ properties0 = {'mappings': mappings}
+ properties1 = {'mappings': mappings,
+ 'root_device_name': root_device1}
+
+ self.assertEqual(block_device.properties_root_device_name({}), None)
+ self.assertEqual(
+ block_device.properties_root_device_name(properties0),
+ root_device0)
+ self.assertEqual(
+ block_device.properties_root_device_name(properties1),
+ root_device1)
+
+ def test_ephemeral(self):
+ self.assertFalse(block_device.is_ephemeral('ephemeral'))
+ self.assertTrue(block_device.is_ephemeral('ephemeral0'))
+ self.assertTrue(block_device.is_ephemeral('ephemeral1'))
+ self.assertTrue(block_device.is_ephemeral('ephemeral11'))
+ self.assertFalse(block_device.is_ephemeral('root'))
+ self.assertFalse(block_device.is_ephemeral('swap'))
+ self.assertFalse(block_device.is_ephemeral('/dev/sda1'))
+
+ self.assertEqual(block_device.ephemeral_num('ephemeral0'), 0)
+ self.assertEqual(block_device.ephemeral_num('ephemeral1'), 1)
+ self.assertEqual(block_device.ephemeral_num('ephemeral11'), 11)
+
+ self.assertFalse(block_device.is_swap_or_ephemeral('ephemeral'))
+ self.assertTrue(block_device.is_swap_or_ephemeral('ephemeral0'))
+ self.assertTrue(block_device.is_swap_or_ephemeral('ephemeral1'))
+ self.assertTrue(block_device.is_swap_or_ephemeral('swap'))
+ self.assertFalse(block_device.is_swap_or_ephemeral('root'))
+ self.assertFalse(block_device.is_swap_or_ephemeral('/dev/sda1'))
+
+ def test_mappings_prepend_dev(self):
+ mapping = [
+ {'virtual': 'ami', 'device': '/dev/sda'},
+ {'virtual': 'root', 'device': 'sda'},
+ {'virtual': 'ephemeral0', 'device': 'sdb'},
+ {'virtual': 'swap', 'device': 'sdc'},
+ {'virtual': 'ephemeral1', 'device': 'sdd'},
+ {'virtual': 'ephemeral2', 'device': 'sde'}]
+
+ expected = [
+ {'virtual': 'ami', 'device': '/dev/sda'},
+ {'virtual': 'root', 'device': 'sda'},
+ {'virtual': 'ephemeral0', 'device': '/dev/sdb'},
+ {'virtual': 'swap', 'device': '/dev/sdc'},
+ {'virtual': 'ephemeral1', 'device': '/dev/sdd'},
+ {'virtual': 'ephemeral2', 'device': '/dev/sde'}]
+
+ prepended = block_device.mappings_prepend_dev(mapping)
+ self.assertEqual(prepended.sort(), expected.sort())
+
+ def test_strip_dev(self):
+ self.assertEqual(block_device.strip_dev('/dev/sda'), 'sda')
+ self.assertEqual(block_device.strip_dev('sda'), 'sda')
diff --git a/nova/tests/test_cloud.py b/nova/tests/test_cloud.py
index 8c1a74c70..b2afc53c9 100644
--- a/nova/tests/test_cloud.py
+++ b/nova/tests/test_cloud.py
@@ -17,6 +17,8 @@
# under the License.
import mox
+import functools
+
from base64 import b64decode
from M2Crypto import BIO
from M2Crypto import RSA
@@ -99,11 +101,9 @@ class CloudTestCase(test.TestCase):
"""Makes sure describe regions runs without raising an exception"""
result = self.cloud.describe_regions(self.context)
self.assertEqual(len(result['regionInfo']), 1)
- regions = FLAGS.region_list
- FLAGS.region_list = ["one=test_host1", "two=test_host2"]
+ self.flags(region_list=["one=test_host1", "two=test_host2"])
result = self.cloud.describe_regions(self.context)
self.assertEqual(len(result['regionInfo']), 2)
- FLAGS.region_list = regions
def test_describe_addresses(self):
"""Makes sure describe addresses runs without raising an exception"""
@@ -894,13 +894,16 @@ class CloudTestCase(test.TestCase):
def test_modify_image_attribute(self):
modify_image_attribute = self.cloud.modify_image_attribute
+ fake_metadata = {'id': 1, 'container_format': 'ami',
+ 'properties': {'kernel_id': 1, 'ramdisk_id': 1,
+ 'type': 'machine'}, 'is_public': False}
+
def fake_show(meh, context, id):
- return {'id': 1, 'container_format': 'ami',
- 'properties': {'kernel_id': 1, 'ramdisk_id': 1,
- 'type': 'machine'}, 'is_public': False}
+ return fake_metadata
def fake_update(meh, context, image_id, metadata, data=None):
- return metadata
+ fake_metadata.update(metadata)
+ return fake_metadata
self.stubs.Set(fake._FakeImageService, 'show', fake_show)
self.stubs.Set(fake._FakeImageService, 'show_by_name', fake_show)
@@ -1466,3 +1469,147 @@ class CloudTestCase(test.TestCase):
# TODO(yamahata): clean up snapshot created by CreateImage.
self._restart_compute_service()
+
+ @staticmethod
+ def _fake_bdm_get(ctxt, id):
+ return [{'volume_id': 87654321,
+ 'snapshot_id': None,
+ 'no_device': None,
+ 'virtual_name': None,
+ 'delete_on_termination': True,
+ 'device_name': '/dev/sdh'},
+ {'volume_id': None,
+ 'snapshot_id': 98765432,
+ 'no_device': None,
+ 'virtual_name': None,
+ 'delete_on_termination': True,
+ 'device_name': '/dev/sdi'},
+ {'volume_id': None,
+ 'snapshot_id': None,
+ 'no_device': True,
+ 'virtual_name': None,
+ 'delete_on_termination': None,
+ 'device_name': None},
+ {'volume_id': None,
+ 'snapshot_id': None,
+ 'no_device': None,
+ 'virtual_name': 'ephemeral0',
+ 'delete_on_termination': None,
+ 'device_name': '/dev/sdb'},
+ {'volume_id': None,
+ 'snapshot_id': None,
+ 'no_device': None,
+ 'virtual_name': 'swap',
+ 'delete_on_termination': None,
+ 'device_name': '/dev/sdc'},
+ {'volume_id': None,
+ 'snapshot_id': None,
+ 'no_device': None,
+ 'virtual_name': 'ephemeral1',
+ 'delete_on_termination': None,
+ 'device_name': '/dev/sdd'},
+ {'volume_id': None,
+ 'snapshot_id': None,
+ 'no_device': None,
+ 'virtual_name': 'ephemeral2',
+ 'delete_on_termination': None,
+ 'device_name': '/dev/sd3'},
+ ]
+
+ def test_get_instance_mapping(self):
+ """Make sure that _get_instance_mapping works"""
+ ctxt = None
+ instance_ref0 = {'id': 0,
+ 'root_device_name': None}
+ instance_ref1 = {'id': 0,
+ 'root_device_name': '/dev/sda1'}
+
+ self.stubs.Set(db, 'block_device_mapping_get_all_by_instance',
+ self._fake_bdm_get)
+
+ expected = {'ami': 'sda1',
+ 'root': '/dev/sda1',
+ 'ephemeral0': '/dev/sdb',
+ 'swap': '/dev/sdc',
+ 'ephemeral1': '/dev/sdd',
+ 'ephemeral2': '/dev/sd3'}
+
+ self.assertEqual(self.cloud._format_instance_mapping(ctxt,
+ instance_ref0),
+ cloud._DEFAULT_MAPPINGS)
+ self.assertEqual(self.cloud._format_instance_mapping(ctxt,
+ instance_ref1),
+ expected)
+
+ def test_describe_instance_attribute(self):
+ """Make sure that describe_instance_attribute works"""
+ self.stubs.Set(db, 'block_device_mapping_get_all_by_instance',
+ self._fake_bdm_get)
+
+ def fake_get(ctxt, instance_id):
+ return {
+ 'id': 0,
+ 'root_device_name': '/dev/sdh',
+ 'security_groups': [{'name': 'fake0'}, {'name': 'fake1'}],
+ 'state_description': 'stopping',
+ 'instance_type': {'name': 'fake_type'},
+ 'kernel_id': 1,
+ 'ramdisk_id': 2,
+ 'user_data': 'fake-user data',
+ }
+ self.stubs.Set(self.cloud.compute_api, 'get', fake_get)
+
+ def fake_volume_get(ctxt, volume_id, session=None):
+ if volume_id == 87654321:
+ return {'id': volume_id,
+ 'attach_time': '13:56:24',
+ 'status': 'in-use'}
+ raise exception.VolumeNotFound(volume_id=volume_id)
+ self.stubs.Set(db.api, 'volume_get', fake_volume_get)
+
+ get_attribute = functools.partial(
+ self.cloud.describe_instance_attribute,
+ self.context, 'i-12345678')
+
+ bdm = get_attribute('blockDeviceMapping')
+ bdm['blockDeviceMapping'].sort()
+
+ expected_bdm = {'instance_id': 'i-12345678',
+ 'rootDeviceType': 'ebs',
+ 'blockDeviceMapping': [
+ {'deviceName': '/dev/sdh',
+ 'ebs': {'status': 'in-use',
+ 'deleteOnTermination': True,
+ 'volumeId': 87654321,
+ 'attachTime': '13:56:24'}}]}
+ expected_bdm['blockDeviceMapping'].sort()
+ self.assertEqual(bdm, expected_bdm)
+ # NOTE(yamahata): this isn't supported
+ # get_attribute('disableApiTermination')
+ groupSet = get_attribute('groupSet')
+ groupSet['groupSet'].sort()
+ expected_groupSet = {'instance_id': 'i-12345678',
+ 'groupSet': [{'groupId': 'fake0'},
+ {'groupId': 'fake1'}]}
+ expected_groupSet['groupSet'].sort()
+ self.assertEqual(groupSet, expected_groupSet)
+ self.assertEqual(get_attribute('instanceInitiatedShutdownBehavior'),
+ {'instance_id': 'i-12345678',
+ 'instanceInitiatedShutdownBehavior': 'stop'})
+ self.assertEqual(get_attribute('instanceType'),
+ {'instance_id': 'i-12345678',
+ 'instanceType': 'fake_type'})
+ self.assertEqual(get_attribute('kernel'),
+ {'instance_id': 'i-12345678',
+ 'kernel': 'aki-00000001'})
+ self.assertEqual(get_attribute('ramdisk'),
+ {'instance_id': 'i-12345678',
+ 'ramdisk': 'ari-00000002'})
+ self.assertEqual(get_attribute('rootDeviceName'),
+ {'instance_id': 'i-12345678',
+ 'rootDeviceName': '/dev/sdh'})
+ # NOTE(yamahata): this isn't supported
+ # get_attribute('sourceDestCheck')
+ self.assertEqual(get_attribute('userData'),
+ {'instance_id': 'i-12345678',
+ 'userData': '}\xa9\x1e\xba\xc7\xabu\xabZ'})
diff --git a/nova/tests/test_compute.py b/nova/tests/test_compute.py
index 879e4b9cb..73c9bd78d 100644
--- a/nova/tests/test_compute.py
+++ b/nova/tests/test_compute.py
@@ -26,6 +26,7 @@ from nova.compute import power_state
from nova import context
from nova import db
from nova.db.sqlalchemy import models
+from nova.db.sqlalchemy import api as sqlalchemy_api
from nova import exception
from nova import flags
import nova.image.fake
@@ -73,8 +74,11 @@ class ComputeTestCase(test.TestCase):
self.stubs.Set(nova.image.fake._FakeImageService, 'show', fake_show)
- def _create_instance(self, params={}):
+ def _create_instance(self, params=None):
"""Create a test instance"""
+ if not params:
+ params = {}
+
inst = {}
inst['image_ref'] = 1
inst['reservation_id'] = 'r-fakeres'
@@ -87,8 +91,11 @@ class ComputeTestCase(test.TestCase):
inst.update(params)
return db.instance_create(self.context, inst)['id']
- def _create_instance_type(self, params={}):
+ def _create_instance_type(self, params=None):
"""Create a test instance"""
+ if not params:
+ params = {}
+
context = self.context.elevated()
inst = {}
inst['name'] = 'm1.small'
@@ -535,7 +542,9 @@ class ComputeTestCase(test.TestCase):
db.instance_update(self.context, instance_id, {'host': 'foo'})
- self.compute.prep_resize(context, inst_ref['uuid'], 3)
+ new_instance_type_ref = db.instance_type_get_by_flavor_id(context, 3)
+ self.compute.prep_resize(context, inst_ref['uuid'],
+ new_instance_type_ref['id'])
migration_ref = db.migration_get_by_instance_and_status(context,
inst_ref['uuid'], 'pre-migrating')
@@ -862,6 +871,458 @@ class ComputeTestCase(test.TestCase):
self.assertEqual(len(instances), 1)
self.assertEqual(power_state.SHUTOFF, instances[0]['state'])
+ def test_get_all_by_name_regexp(self):
+ """Test searching instances by name (display_name)"""
+ c = context.get_admin_context()
+ instance_id1 = self._create_instance({'display_name': 'woot'})
+ instance_id2 = self._create_instance({
+ 'display_name': 'woo',
+ 'id': 20})
+ instance_id3 = self._create_instance({
+ 'display_name': 'not-woot',
+ 'id': 30})
+
+ instances = self.compute_api.get_all(c,
+ search_opts={'name': 'woo.*'})
+ self.assertEqual(len(instances), 2)
+ instance_ids = [instance.id for instance in instances]
+ self.assertTrue(instance_id1 in instance_ids)
+ self.assertTrue(instance_id2 in instance_ids)
+
+ instances = self.compute_api.get_all(c,
+ search_opts={'name': 'woot.*'})
+ instance_ids = [instance.id for instance in instances]
+ self.assertEqual(len(instances), 1)
+ self.assertTrue(instance_id1 in instance_ids)
+
+ instances = self.compute_api.get_all(c,
+ search_opts={'name': '.*oot.*'})
+ self.assertEqual(len(instances), 2)
+ instance_ids = [instance.id for instance in instances]
+ self.assertTrue(instance_id1 in instance_ids)
+ self.assertTrue(instance_id3 in instance_ids)
+
+ instances = self.compute_api.get_all(c,
+ search_opts={'name': 'n.*'})
+ self.assertEqual(len(instances), 1)
+ instance_ids = [instance.id for instance in instances]
+ self.assertTrue(instance_id3 in instance_ids)
+
+ instances = self.compute_api.get_all(c,
+ search_opts={'name': 'noth.*'})
+ self.assertEqual(len(instances), 0)
+
+ db.instance_destroy(c, instance_id1)
+ db.instance_destroy(c, instance_id2)
+ db.instance_destroy(c, instance_id3)
+
+ def test_get_all_by_instance_name_regexp(self):
+ """Test searching instances by name"""
+ self.flags(instance_name_template='instance-%d')
+
+ c = context.get_admin_context()
+ instance_id1 = self._create_instance()
+ instance_id2 = self._create_instance({'id': 2})
+ instance_id3 = self._create_instance({'id': 10})
+
+ instances = self.compute_api.get_all(c,
+ search_opts={'instance_name': 'instance.*'})
+ self.assertEqual(len(instances), 3)
+
+ instances = self.compute_api.get_all(c,
+ search_opts={'instance_name': '.*\-\d$'})
+ self.assertEqual(len(instances), 2)
+ instance_ids = [instance.id for instance in instances]
+ self.assertTrue(instance_id1 in instance_ids)
+ self.assertTrue(instance_id2 in instance_ids)
+
+ instances = self.compute_api.get_all(c,
+ search_opts={'instance_name': 'i.*2'})
+ self.assertEqual(len(instances), 1)
+ self.assertEqual(instances[0].id, instance_id2)
+
+ db.instance_destroy(c, instance_id1)
+ db.instance_destroy(c, instance_id2)
+ db.instance_destroy(c, instance_id3)
+
+ def test_get_by_fixed_ip(self):
+ """Test getting 1 instance by Fixed IP"""
+ c = context.get_admin_context()
+ instance_id1 = self._create_instance()
+ instance_id2 = self._create_instance({'id': 20})
+ instance_id3 = self._create_instance({'id': 30})
+
+ vif_ref1 = db.virtual_interface_create(c,
+ {'address': '12:34:56:78:90:12',
+ 'instance_id': instance_id1,
+ 'network_id': 1})
+ vif_ref2 = db.virtual_interface_create(c,
+ {'address': '90:12:34:56:78:90',
+ 'instance_id': instance_id2,
+ 'network_id': 1})
+
+ db.fixed_ip_create(c,
+ {'address': '1.1.1.1',
+ 'instance_id': instance_id1,
+ 'virtual_interface_id': vif_ref1['id']})
+ db.fixed_ip_create(c,
+ {'address': '1.1.2.1',
+ 'instance_id': instance_id2,
+ 'virtual_interface_id': vif_ref2['id']})
+
+ # regex not allowed
+ instances = self.compute_api.get_all(c,
+ search_opts={'fixed_ip': '.*'})
+ self.assertEqual(len(instances), 0)
+
+ instances = self.compute_api.get_all(c,
+ search_opts={'fixed_ip': '1.1.3.1'})
+ self.assertEqual(len(instances), 0)
+
+ instances = self.compute_api.get_all(c,
+ search_opts={'fixed_ip': '1.1.1.1'})
+ self.assertEqual(len(instances), 1)
+ self.assertEqual(instances[0].id, instance_id1)
+
+ instances = self.compute_api.get_all(c,
+ search_opts={'fixed_ip': '1.1.2.1'})
+ self.assertEqual(len(instances), 1)
+ self.assertEqual(instances[0].id, instance_id2)
+
+ db.virtual_interface_delete(c, vif_ref1['id'])
+ db.virtual_interface_delete(c, vif_ref2['id'])
+ db.instance_destroy(c, instance_id1)
+ db.instance_destroy(c, instance_id2)
+
+ def test_get_all_by_ip_regexp(self):
+ """Test searching by Floating and Fixed IP"""
+ c = context.get_admin_context()
+ instance_id1 = self._create_instance({'display_name': 'woot'})
+ instance_id2 = self._create_instance({
+ 'display_name': 'woo',
+ 'id': 20})
+ instance_id3 = self._create_instance({
+ 'display_name': 'not-woot',
+ 'id': 30})
+
+ vif_ref1 = db.virtual_interface_create(c,
+ {'address': '12:34:56:78:90:12',
+ 'instance_id': instance_id1,
+ 'network_id': 1})
+ vif_ref2 = db.virtual_interface_create(c,
+ {'address': '90:12:34:56:78:90',
+ 'instance_id': instance_id2,
+ 'network_id': 1})
+ vif_ref3 = db.virtual_interface_create(c,
+ {'address': '34:56:78:90:12:34',
+ 'instance_id': instance_id3,
+ 'network_id': 1})
+
+ db.fixed_ip_create(c,
+ {'address': '1.1.1.1',
+ 'instance_id': instance_id1,
+ 'virtual_interface_id': vif_ref1['id']})
+ db.fixed_ip_create(c,
+ {'address': '1.1.2.1',
+ 'instance_id': instance_id2,
+ 'virtual_interface_id': vif_ref2['id']})
+ fix_addr = db.fixed_ip_create(c,
+ {'address': '1.1.3.1',
+ 'instance_id': instance_id3,
+ 'virtual_interface_id': vif_ref3['id']})
+ fix_ref = db.fixed_ip_get_by_address(c, fix_addr)
+ flo_ref = db.floating_ip_create(c,
+ {'address': '10.0.0.2',
+ 'fixed_ip_id': fix_ref['id']})
+
+ # ends up matching 2nd octet here.. so all 3 match
+ instances = self.compute_api.get_all(c,
+ search_opts={'ip': '.*\.1'})
+ self.assertEqual(len(instances), 3)
+
+ instances = self.compute_api.get_all(c,
+ search_opts={'ip': '1.*'})
+ self.assertEqual(len(instances), 3)
+
+ instances = self.compute_api.get_all(c,
+ search_opts={'ip': '.*\.1.\d+$'})
+ self.assertEqual(len(instances), 1)
+ instance_ids = [instance.id for instance in instances]
+ self.assertTrue(instance_id1 in instance_ids)
+
+ instances = self.compute_api.get_all(c,
+ search_opts={'ip': '.*\.2.+'})
+ self.assertEqual(len(instances), 1)
+ self.assertEqual(instances[0].id, instance_id2)
+
+ instances = self.compute_api.get_all(c,
+ search_opts={'ip': '10.*'})
+ self.assertEqual(len(instances), 1)
+ self.assertEqual(instances[0].id, instance_id3)
+
+ db.virtual_interface_delete(c, vif_ref1['id'])
+ db.virtual_interface_delete(c, vif_ref2['id'])
+ db.virtual_interface_delete(c, vif_ref3['id'])
+ db.floating_ip_destroy(c, '10.0.0.2')
+ db.instance_destroy(c, instance_id1)
+ db.instance_destroy(c, instance_id2)
+ db.instance_destroy(c, instance_id3)
+
+ def test_get_all_by_ipv6_regexp(self):
+ """Test searching by IPv6 address"""
+
+ c = context.get_admin_context()
+ instance_id1 = self._create_instance({'display_name': 'woot'})
+ instance_id2 = self._create_instance({
+ 'display_name': 'woo',
+ 'id': 20})
+ instance_id3 = self._create_instance({
+ 'display_name': 'not-woot',
+ 'id': 30})
+
+ vif_ref1 = db.virtual_interface_create(c,
+ {'address': '12:34:56:78:90:12',
+ 'instance_id': instance_id1,
+ 'network_id': 1})
+ vif_ref2 = db.virtual_interface_create(c,
+ {'address': '90:12:34:56:78:90',
+ 'instance_id': instance_id2,
+ 'network_id': 1})
+ vif_ref3 = db.virtual_interface_create(c,
+ {'address': '34:56:78:90:12:34',
+ 'instance_id': instance_id3,
+ 'network_id': 1})
+
+ # This will create IPv6 addresses of:
+ # 1: fd00::1034:56ff:fe78:9012
+ # 20: fd00::9212:34ff:fe56:7890
+ # 30: fd00::3656:78ff:fe90:1234
+
+ instances = self.compute_api.get_all(c,
+ search_opts={'ip6': '.*1034.*'})
+ self.assertEqual(len(instances), 1)
+ self.assertEqual(instances[0].id, instance_id1)
+
+ instances = self.compute_api.get_all(c,
+ search_opts={'ip6': '^fd00.*'})
+ self.assertEqual(len(instances), 3)
+ instance_ids = [instance.id for instance in instances]
+ self.assertTrue(instance_id1 in instance_ids)
+ self.assertTrue(instance_id2 in instance_ids)
+ self.assertTrue(instance_id3 in instance_ids)
+
+ instances = self.compute_api.get_all(c,
+ search_opts={'ip6': '^.*12.*34.*'})
+ self.assertEqual(len(instances), 2)
+ instance_ids = [instance.id for instance in instances]
+ self.assertTrue(instance_id2 in instance_ids)
+ self.assertTrue(instance_id3 in instance_ids)
+
+ db.virtual_interface_delete(c, vif_ref1['id'])
+ db.virtual_interface_delete(c, vif_ref2['id'])
+ db.virtual_interface_delete(c, vif_ref3['id'])
+ db.instance_destroy(c, instance_id1)
+ db.instance_destroy(c, instance_id2)
+ db.instance_destroy(c, instance_id3)
+
+ def test_get_all_by_multiple_options_at_once(self):
+ """Test searching by multiple options at once"""
+ c = context.get_admin_context()
+ instance_id1 = self._create_instance({'display_name': 'woot'})
+ instance_id2 = self._create_instance({
+ 'display_name': 'woo',
+ 'id': 20})
+ instance_id3 = self._create_instance({
+ 'display_name': 'not-woot',
+ 'id': 30})
+
+ vif_ref1 = db.virtual_interface_create(c,
+ {'address': '12:34:56:78:90:12',
+ 'instance_id': instance_id1,
+ 'network_id': 1})
+ vif_ref2 = db.virtual_interface_create(c,
+ {'address': '90:12:34:56:78:90',
+ 'instance_id': instance_id2,
+ 'network_id': 1})
+ vif_ref3 = db.virtual_interface_create(c,
+ {'address': '34:56:78:90:12:34',
+ 'instance_id': instance_id3,
+ 'network_id': 1})
+
+ db.fixed_ip_create(c,
+ {'address': '1.1.1.1',
+ 'instance_id': instance_id1,
+ 'virtual_interface_id': vif_ref1['id']})
+ db.fixed_ip_create(c,
+ {'address': '1.1.2.1',
+ 'instance_id': instance_id2,
+ 'virtual_interface_id': vif_ref2['id']})
+ fix_addr = db.fixed_ip_create(c,
+ {'address': '1.1.3.1',
+ 'instance_id': instance_id3,
+ 'virtual_interface_id': vif_ref3['id']})
+ fix_ref = db.fixed_ip_get_by_address(c, fix_addr)
+ flo_ref = db.floating_ip_create(c,
+ {'address': '10.0.0.2',
+ 'fixed_ip_id': fix_ref['id']})
+
+ # ip ends up matching 2nd octet here.. so all 3 match ip
+ # but 'name' only matches one
+ instances = self.compute_api.get_all(c,
+ search_opts={'ip': '.*\.1', 'name': 'not.*'})
+ self.assertEqual(len(instances), 1)
+ self.assertEqual(instances[0].id, instance_id3)
+
+ # ip ends up matching any ip with a '2' in it.. so instance
+ # 2 and 3.. but name should only match #2
+ # but 'name' only matches one
+ instances = self.compute_api.get_all(c,
+ search_opts={'ip': '.*2', 'name': '^woo.*'})
+ self.assertEqual(len(instances), 1)
+ self.assertEqual(instances[0].id, instance_id2)
+
+ # same as above but no match on name (name matches instance_id1
+ # but the ip query doesn't
+ instances = self.compute_api.get_all(c,
+ search_opts={'ip': '.*2.*', 'name': '^woot.*'})
+ self.assertEqual(len(instances), 0)
+
+ # ip matches all 3... ipv6 matches #2+#3...name matches #3
+ instances = self.compute_api.get_all(c,
+ search_opts={'ip': '.*\.1',
+ 'name': 'not.*',
+ 'ip6': '^.*12.*34.*'})
+ self.assertEqual(len(instances), 1)
+ self.assertEqual(instances[0].id, instance_id3)
+
+ db.virtual_interface_delete(c, vif_ref1['id'])
+ db.virtual_interface_delete(c, vif_ref2['id'])
+ db.virtual_interface_delete(c, vif_ref3['id'])
+ db.floating_ip_destroy(c, '10.0.0.2')
+ db.instance_destroy(c, instance_id1)
+ db.instance_destroy(c, instance_id2)
+ db.instance_destroy(c, instance_id3)
+
+ def test_get_all_by_image(self):
+ """Test searching instances by image"""
+
+ c = context.get_admin_context()
+ instance_id1 = self._create_instance({'image_ref': '1234'})
+ instance_id2 = self._create_instance({
+ 'id': 2,
+ 'image_ref': '4567'})
+ instance_id3 = self._create_instance({
+ 'id': 10,
+ 'image_ref': '4567'})
+
+ instances = self.compute_api.get_all(c,
+ search_opts={'image': '123'})
+ self.assertEqual(len(instances), 0)
+
+ instances = self.compute_api.get_all(c,
+ search_opts={'image': '1234'})
+ self.assertEqual(len(instances), 1)
+ self.assertEqual(instances[0].id, instance_id1)
+
+ instances = self.compute_api.get_all(c,
+ search_opts={'image': '4567'})
+ self.assertEqual(len(instances), 2)
+ instance_ids = [instance.id for instance in instances]
+ self.assertTrue(instance_id2 in instance_ids)
+ self.assertTrue(instance_id3 in instance_ids)
+
+ # Test passing a list as search arg
+ instances = self.compute_api.get_all(c,
+ search_opts={'image': ['1234', '4567']})
+ self.assertEqual(len(instances), 3)
+
+ db.instance_destroy(c, instance_id1)
+ db.instance_destroy(c, instance_id2)
+ db.instance_destroy(c, instance_id3)
+
+ def test_get_all_by_flavor(self):
+ """Test searching instances by image"""
+
+ c = context.get_admin_context()
+ instance_id1 = self._create_instance({'instance_type_id': 1})
+ instance_id2 = self._create_instance({
+ 'id': 2,
+ 'instance_type_id': 2})
+ instance_id3 = self._create_instance({
+ 'id': 10,
+ 'instance_type_id': 2})
+
+ # NOTE(comstud): Migrations set up the instance_types table
+ # for us. Therefore, we assume the following is true for
+ # these tests:
+ # instance_type_id 1 == flavor 3
+ # instance_type_id 2 == flavor 1
+ # instance_type_id 3 == flavor 4
+ # instance_type_id 4 == flavor 5
+ # instance_type_id 5 == flavor 2
+
+ instances = self.compute_api.get_all(c,
+ search_opts={'flavor': 5})
+ self.assertEqual(len(instances), 0)
+
+ self.assertRaises(exception.FlavorNotFound,
+ self.compute_api.get_all,
+ c, search_opts={'flavor': 99})
+
+ instances = self.compute_api.get_all(c,
+ search_opts={'flavor': 3})
+ self.assertEqual(len(instances), 1)
+ self.assertEqual(instances[0].id, instance_id1)
+
+ instances = self.compute_api.get_all(c,
+ search_opts={'flavor': 1})
+ self.assertEqual(len(instances), 2)
+ instance_ids = [instance.id for instance in instances]
+ self.assertTrue(instance_id2 in instance_ids)
+ self.assertTrue(instance_id3 in instance_ids)
+
+ db.instance_destroy(c, instance_id1)
+ db.instance_destroy(c, instance_id2)
+ db.instance_destroy(c, instance_id3)
+
+ def test_get_all_by_state(self):
+ """Test searching instances by state"""
+
+ c = context.get_admin_context()
+ instance_id1 = self._create_instance({'state': power_state.SHUTDOWN})
+ instance_id2 = self._create_instance({
+ 'id': 2,
+ 'state': power_state.RUNNING})
+ instance_id3 = self._create_instance({
+ 'id': 10,
+ 'state': power_state.RUNNING})
+
+ instances = self.compute_api.get_all(c,
+ search_opts={'state': power_state.SUSPENDED})
+ self.assertEqual(len(instances), 0)
+
+ instances = self.compute_api.get_all(c,
+ search_opts={'state': power_state.SHUTDOWN})
+ self.assertEqual(len(instances), 1)
+ self.assertEqual(instances[0].id, instance_id1)
+
+ instances = self.compute_api.get_all(c,
+ search_opts={'state': power_state.RUNNING})
+ self.assertEqual(len(instances), 2)
+ instance_ids = [instance.id for instance in instances]
+ self.assertTrue(instance_id2 in instance_ids)
+ self.assertTrue(instance_id3 in instance_ids)
+
+ # Test passing a list as search arg
+ instances = self.compute_api.get_all(c,
+ search_opts={'state': [power_state.SHUTDOWN,
+ power_state.RUNNING]})
+ self.assertEqual(len(instances), 3)
+
+ db.instance_destroy(c, instance_id1)
+ db.instance_destroy(c, instance_id2)
+ db.instance_destroy(c, instance_id3)
+
@staticmethod
def _parse_db_block_device_mapping(bdm_ref):
attr_list = ('delete_on_termination', 'device_name', 'no_device',
@@ -875,15 +1336,17 @@ class ComputeTestCase(test.TestCase):
return bdm
def test_update_block_device_mapping(self):
+ swap_size = 1
+ instance_type = {'swap': swap_size}
instance_id = self._create_instance()
mappings = [
{'virtual': 'ami', 'device': 'sda1'},
{'virtual': 'root', 'device': '/dev/sda1'},
- {'virtual': 'swap', 'device': 'sdb1'},
- {'virtual': 'swap', 'device': 'sdb2'},
- {'virtual': 'swap', 'device': 'sdb3'},
{'virtual': 'swap', 'device': 'sdb4'},
+ {'virtual': 'swap', 'device': 'sdb3'},
+ {'virtual': 'swap', 'device': 'sdb2'},
+ {'virtual': 'swap', 'device': 'sdb1'},
{'virtual': 'ephemeral0', 'device': 'sdc1'},
{'virtual': 'ephemeral1', 'device': 'sdc2'},
@@ -925,32 +1388,36 @@ class ComputeTestCase(test.TestCase):
'no_device': True}]
self.compute_api._update_image_block_device_mapping(
- self.context, instance_id, mappings)
+ self.context, instance_type, instance_id, mappings)
bdms = [self._parse_db_block_device_mapping(bdm_ref)
for bdm_ref in db.block_device_mapping_get_all_by_instance(
self.context, instance_id)]
expected_result = [
- {'virtual_name': 'swap', 'device_name': '/dev/sdb1'},
- {'virtual_name': 'swap', 'device_name': '/dev/sdb2'},
- {'virtual_name': 'swap', 'device_name': '/dev/sdb3'},
- {'virtual_name': 'swap', 'device_name': '/dev/sdb4'},
+ {'virtual_name': 'swap', 'device_name': '/dev/sdb1',
+ 'volume_size': swap_size},
{'virtual_name': 'ephemeral0', 'device_name': '/dev/sdc1'},
- {'virtual_name': 'ephemeral1', 'device_name': '/dev/sdc2'},
- {'virtual_name': 'ephemeral2', 'device_name': '/dev/sdc3'}]
+
+ # NOTE(yamahata): ATM only ephemeral0 is supported.
+ # they're ignored for now
+ #{'virtual_name': 'ephemeral1', 'device_name': '/dev/sdc2'},
+ #{'virtual_name': 'ephemeral2', 'device_name': '/dev/sdc3'}
+ ]
bdms.sort()
expected_result.sort()
self.assertDictListMatch(bdms, expected_result)
self.compute_api._update_block_device_mapping(
- self.context, instance_id, block_device_mapping)
+ self.context, instance_types.get_default_instance_type(),
+ instance_id, block_device_mapping)
bdms = [self._parse_db_block_device_mapping(bdm_ref)
for bdm_ref in db.block_device_mapping_get_all_by_instance(
self.context, instance_id)]
expected_result = [
{'snapshot_id': 0x12345678, 'device_name': '/dev/sda1'},
- {'virtual_name': 'swap', 'device_name': '/dev/sdb1'},
+ {'virtual_name': 'swap', 'device_name': '/dev/sdb1',
+ 'volume_size': swap_size},
{'snapshot_id': 0x23456789, 'device_name': '/dev/sdb2'},
{'snapshot_id': 0x3456789A, 'device_name': '/dev/sdb3'},
{'no_device': True, 'device_name': '/dev/sdb4'},
@@ -972,3 +1439,13 @@ class ComputeTestCase(test.TestCase):
self.context, instance_id):
db.block_device_mapping_destroy(self.context, bdm['id'])
self.compute.terminate_instance(self.context, instance_id)
+
+ def test_ephemeral_size(self):
+ local_size = 2
+ inst_type = {'local_gb': local_size}
+ self.assertEqual(self.compute_api._ephemeral_size(inst_type,
+ 'ephemeral0'),
+ local_size)
+ self.assertEqual(self.compute_api._ephemeral_size(inst_type,
+ 'ephemeral1'),
+ 0)
diff --git a/nova/tests/test_host_filter.py b/nova/tests/test_host_filter.py
index 438f3e522..3a1389a49 100644
--- a/nova/tests/test_host_filter.py
+++ b/nova/tests/test_host_filter.py
@@ -19,12 +19,9 @@ Tests For Scheduler Host Filters.
import json
from nova import exception
-from nova import flags
from nova import test
from nova.scheduler import host_filter
-FLAGS = flags.FLAGS
-
class FakeZoneManager:
pass
@@ -57,9 +54,9 @@ class HostFilterTestCase(test.TestCase):
'host_name-label': 'xs-%s' % multiplier}
def setUp(self):
- self.old_flag = FLAGS.default_host_filter
- FLAGS.default_host_filter = \
- 'nova.scheduler.host_filter.AllHostsFilter'
+ super(HostFilterTestCase, self).setUp()
+ default_host_filter = 'nova.scheduler.host_filter.AllHostsFilter'
+ self.flags(default_host_filter=default_host_filter)
self.instance_type = dict(name='tiny',
memory_mb=50,
vcpus=10,
@@ -76,9 +73,6 @@ class HostFilterTestCase(test.TestCase):
states['host%02d' % (x + 1)] = {'compute': self._host_caps(x)}
self.zone_manager.service_states = states
- def tearDown(self):
- FLAGS.default_host_filter = self.old_flag
-
def test_choose_filter(self):
# Test default filter ...
hf = host_filter.choose_host_filter()
diff --git a/nova/tests/test_hosts.py b/nova/tests/test_hosts.py
index 548f81f8b..a724db9da 100644
--- a/nova/tests/test_hosts.py
+++ b/nova/tests/test_hosts.py
@@ -48,6 +48,10 @@ def stub_set_host_enabled(context, host, enabled):
return status
+def stub_host_power_action(context, host, action):
+ return action
+
+
class FakeRequest(object):
environ = {"nova.context": context.get_admin_context()}
@@ -62,6 +66,8 @@ class HostTestCase(test.TestCase):
self.stubs.Set(scheduler_api, 'get_host_list', stub_get_host_list)
self.stubs.Set(self.controller.compute_api, 'set_host_enabled',
stub_set_host_enabled)
+ self.stubs.Set(self.controller.compute_api, 'host_power_action',
+ stub_host_power_action)
def test_list_hosts(self):
"""Verify that the compute hosts are returned."""
@@ -87,6 +93,18 @@ class HostTestCase(test.TestCase):
result_c2 = self.controller.update(self.req, "host_c2", body=en_body)
self.assertEqual(result_c2["status"], "disabled")
+ def test_host_startup(self):
+ result = self.controller.startup(self.req, "host_c1")
+ self.assertEqual(result["power_action"], "startup")
+
+ def test_host_shutdown(self):
+ result = self.controller.shutdown(self.req, "host_c1")
+ self.assertEqual(result["power_action"], "shutdown")
+
+ def test_host_reboot(self):
+ result = self.controller.reboot(self.req, "host_c1")
+ self.assertEqual(result["power_action"], "reboot")
+
def test_bad_status_value(self):
bad_body = {"status": "bad"}
self.assertRaises(webob.exc.HTTPBadRequest, self.controller.update,
diff --git a/nova/tests/test_image.py b/nova/tests/test_image.py
new file mode 100644
index 000000000..9680d6f2b
--- /dev/null
+++ b/nova/tests/test_image.py
@@ -0,0 +1,134 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2011 OpenStack LLC
+# Author: Soren Hansen
+#
+# 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 datetime
+
+from nova import context
+from nova import exception
+from nova import test
+import nova.image
+
+
+class _ImageTestCase(test.TestCase):
+ def setUp(self):
+ super(_ImageTestCase, self).setUp()
+ self.context = context.get_admin_context()
+
+ def test_index(self):
+ res = self.image_service.index(self.context)
+ for image in res:
+ self.assertEquals(set(image.keys()), set(['id', 'name']))
+
+ def test_detail(self):
+ res = self.image_service.detail(self.context)
+ for image in res:
+ keys = set(image.keys())
+ self.assertEquals(keys, set(['id', 'name', 'created_at',
+ 'updated_at', 'deleted_at', 'deleted',
+ 'status', 'is_public', 'properties']))
+ self.assertTrue(isinstance(image['created_at'], datetime.datetime))
+ self.assertTrue(isinstance(image['updated_at'], datetime.datetime))
+
+ if not (isinstance(image['deleted_at'], datetime.datetime) or
+ image['deleted_at'] is None):
+ self.fail('image\'s "deleted_at" attribute was neither a '
+ 'datetime object nor None')
+
+ def check_is_bool(image, key):
+ val = image.get('deleted')
+ if not isinstance(val, bool):
+ self.fail('image\'s "%s" attribute wasn\'t '
+ 'a bool: %r' % (key, val))
+
+ check_is_bool(image, 'deleted')
+ check_is_bool(image, 'is_public')
+
+ def test_index_and_detail_have_same_results(self):
+ index = self.image_service.index(self.context)
+ detail = self.image_service.detail(self.context)
+ index_set = set([(i['id'], i['name']) for i in index])
+ detail_set = set([(i['id'], i['name']) for i in detail])
+ self.assertEqual(index_set, detail_set)
+
+ def test_show_raises_imagenotfound_for_invalid_id(self):
+ self.assertRaises(exception.ImageNotFound,
+ self.image_service.show,
+ self.context,
+ 'this image does not exist')
+
+ def test_show_by_name(self):
+ self.assertRaises(exception.ImageNotFound,
+ self.image_service.show_by_name,
+ self.context,
+ 'this image does not exist')
+
+ def test_create_adds_id(self):
+ index = self.image_service.index(self.context)
+ image_count = len(index)
+
+ self.image_service.create(self.context, {})
+
+ index = self.image_service.index(self.context)
+ self.assertEquals(len(index), image_count + 1)
+
+ self.assertTrue(index[0]['id'])
+
+ def test_create_keeps_id(self):
+ self.image_service.create(self.context, {'id': '34'})
+ self.image_service.show(self.context, '34')
+
+ def test_create_rejects_duplicate_ids(self):
+ self.image_service.create(self.context, {'id': '34'})
+ self.assertRaises(exception.Duplicate,
+ self.image_service.create,
+ self.context,
+ {'id': '34'})
+
+ # Make sure there's still one left
+ self.image_service.show(self.context, '34')
+
+ def test_update(self):
+ self.image_service.create(self.context,
+ {'id': '34', 'foo': 'bar'})
+
+ self.image_service.update(self.context, '34',
+ {'id': '34', 'foo': 'baz'})
+
+ img = self.image_service.show(self.context, '34')
+ self.assertEquals(img['foo'], 'baz')
+
+ def test_delete(self):
+ self.image_service.create(self.context, {'id': '34', 'foo': 'bar'})
+ self.image_service.delete(self.context, '34')
+ self.assertRaises(exception.NotFound,
+ self.image_service.show,
+ self.context,
+ '34')
+
+ def test_delete_all(self):
+ self.image_service.create(self.context, {'id': '32', 'foo': 'bar'})
+ self.image_service.create(self.context, {'id': '33', 'foo': 'bar'})
+ self.image_service.create(self.context, {'id': '34', 'foo': 'bar'})
+ self.image_service.delete_all()
+ index = self.image_service.index(self.context)
+ self.assertEquals(len(index), 0)
+
+
+class FakeImageTestCase(_ImageTestCase):
+ def setUp(self):
+ super(FakeImageTestCase, self).setUp()
+ self.image_service = nova.image.fake.FakeImageService()
diff --git a/nova/tests/test_instance_types_extra_specs.py b/nova/tests/test_instance_types_extra_specs.py
index 393ed1e36..205601277 100644
--- a/nova/tests/test_instance_types_extra_specs.py
+++ b/nova/tests/test_instance_types_extra_specs.py
@@ -136,7 +136,7 @@ class InstanceTypeExtraSpecsTestCase(test.TestCase):
"m1.small")
self.assertEquals(instance_type['extra_specs'], {})
- def test_instance_type_get_with_extra_specs(self):
+ def test_instance_type_get_by_flavor_id_with_extra_specs(self):
instance_type = db.api.instance_type_get_by_flavor_id(
context.get_admin_context(),
105)
diff --git a/nova/tests/test_ipv6.py b/nova/tests/test_ipv6.py
index 11dc2ec98..d123df6f1 100644
--- a/nova/tests/test_ipv6.py
+++ b/nova/tests/test_ipv6.py
@@ -16,15 +16,12 @@
"""Test suite for IPv6."""
-from nova import flags
from nova import ipv6
from nova import log as logging
from nova import test
LOG = logging.getLogger('nova.tests.test_ipv6')
-FLAGS = flags.FLAGS
-
import sys
diff --git a/nova/tests/test_libvirt.py b/nova/tests/test_libvirt.py
index cf25ce215..2180cf4f0 100644
--- a/nova/tests/test_libvirt.py
+++ b/nova/tests/test_libvirt.py
@@ -38,7 +38,6 @@ from nova.virt.libvirt import firewall
libvirt = None
FLAGS = flags.FLAGS
-flags.DECLARE('instances_path', 'nova.compute.manager')
def _concurrency(wait, done, target):
@@ -93,6 +92,7 @@ def _setup_networking(instance_id, ip='1.2.3.4'):
class CacheConcurrencyTestCase(test.TestCase):
def setUp(self):
super(CacheConcurrencyTestCase, self).setUp()
+ self.flags(instances_path='nova.compute.manager')
def fake_exists(fname):
basedir = os.path.join(FLAGS.instances_path, '_base')
@@ -158,7 +158,7 @@ class LibvirtConnTestCase(test.TestCase):
self.context = context.RequestContext(self.user_id, self.project_id)
self.network = utils.import_object(FLAGS.network_manager)
self.context = context.get_admin_context()
- FLAGS.instances_path = ''
+ self.flags(instances_path='')
self.call_libvirt_dependant_setup = False
self.test_ip = '10.11.12.13'
@@ -169,6 +169,7 @@ class LibvirtConnTestCase(test.TestCase):
'project_id': 'fake',
'bridge': 'br101',
'image_ref': '123456',
+ 'local_gb': 20,
'instance_type_id': '5'} # m1.small
def lazy_load_library_exists(self):
@@ -322,7 +323,7 @@ class LibvirtConnTestCase(test.TestCase):
if not self.lazy_load_library_exists():
return
- FLAGS.image_service = 'nova.image.fake.FakeImageService'
+ self.flags(image_service='nova.image.fake.FakeImageService')
# Start test
image_service = utils.import_object(FLAGS.image_service)
@@ -357,7 +358,7 @@ class LibvirtConnTestCase(test.TestCase):
if not self.lazy_load_library_exists():
return
- FLAGS.image_service = 'nova.image.fake.FakeImageService'
+ self.flags(image_service='nova.image.fake.FakeImageService')
# Start test
image_service = utils.import_object(FLAGS.image_service)
@@ -521,7 +522,7 @@ class LibvirtConnTestCase(test.TestCase):
'disk.local')]
for (libvirt_type, (expected_uri, checks)) in type_uri_map.iteritems():
- FLAGS.libvirt_type = libvirt_type
+ self.flags(libvirt_type=libvirt_type)
conn = connection.LibvirtConnection(True)
uri = conn.get_uri()
@@ -546,9 +547,9 @@ class LibvirtConnTestCase(test.TestCase):
# checking against that later on. This way we make sure the
# implementation doesn't fiddle around with the FLAGS.
testuri = 'something completely different'
- FLAGS.libvirt_uri = testuri
+ self.flags(libvirt_uri=testuri)
for (libvirt_type, (expected_uri, checks)) in type_uri_map.iteritems():
- FLAGS.libvirt_type = libvirt_type
+ self.flags(libvirt_type=libvirt_type)
conn = connection.LibvirtConnection(True)
uri = conn.get_uri()
self.assertEquals(uri, testuri)
@@ -556,8 +557,7 @@ class LibvirtConnTestCase(test.TestCase):
def test_update_available_resource_works_correctly(self):
"""Confirm compute_node table is updated successfully."""
- org_path = FLAGS.instances_path = ''
- FLAGS.instances_path = '.'
+ self.flags(instances_path='.')
# Prepare mocks
def getVersion():
@@ -604,12 +604,10 @@ class LibvirtConnTestCase(test.TestCase):
self.assertTrue(compute_node['hypervisor_version'] > 0)
db.service_destroy(self.context, service_ref['id'])
- FLAGS.instances_path = org_path
def test_update_resource_info_no_compute_record_found(self):
"""Raise exception if no recorde found on services table."""
- org_path = FLAGS.instances_path = ''
- FLAGS.instances_path = '.'
+ self.flags(instances_path='.')
self.create_fake_libvirt_mock()
self.mox.ReplayAll()
@@ -618,8 +616,6 @@ class LibvirtConnTestCase(test.TestCase):
conn.update_available_resource,
self.context, 'dummy')
- FLAGS.instances_path = org_path
-
def test_ensure_filtering_rules_for_instance_timeout(self):
"""ensure_filtering_fules_for_instance() finishes with timeout."""
# Skip if non-libvirt environment
@@ -749,6 +745,42 @@ class LibvirtConnTestCase(test.TestCase):
ip = conn.get_host_ip_addr()
self.assertEquals(ip, FLAGS.my_ip)
+ def test_volume_in_mapping(self):
+ conn = connection.LibvirtConnection(False)
+ swap = {'device_name': '/dev/sdb',
+ 'swap_size': 1}
+ ephemerals = [{'num': 0,
+ 'virtual_name': 'ephemeral0',
+ 'device_name': '/dev/sdc1',
+ 'size': 1},
+ {'num': 2,
+ 'virtual_name': 'ephemeral2',
+ 'device_name': '/dev/sdd',
+ 'size': 1}]
+ block_device_mapping = [{'mount_device': '/dev/sde',
+ 'device_path': 'fake_device'},
+ {'mount_device': '/dev/sdf',
+ 'device_path': 'fake_device'}]
+ block_device_info = {
+ 'root_device_name': '/dev/sda',
+ 'swap': swap,
+ 'ephemerals': ephemerals,
+ 'block_device_mapping': block_device_mapping}
+
+ def _assert_volume_in_mapping(device_name, true_or_false):
+ self.assertEquals(conn._volume_in_mapping(device_name,
+ block_device_info),
+ true_or_false)
+
+ _assert_volume_in_mapping('sda', False)
+ _assert_volume_in_mapping('sdb', True)
+ _assert_volume_in_mapping('sdc1', True)
+ _assert_volume_in_mapping('sdd', True)
+ _assert_volume_in_mapping('sde', True)
+ _assert_volume_in_mapping('sdf', True)
+ _assert_volume_in_mapping('sdg', False)
+ _assert_volume_in_mapping('sdh1', False)
+
class NWFilterFakes:
def __init__(self):
@@ -889,18 +921,18 @@ class IptablesFirewallTestCase(test.TestCase):
# self.fw.add_instance(instance_ref)
def fake_iptables_execute(*cmd, **kwargs):
process_input = kwargs.get('process_input', None)
- if cmd == ('sudo', 'ip6tables-save', '-t', 'filter'):
+ if cmd == ('ip6tables-save', '-t', 'filter'):
return '\n'.join(self.in6_filter_rules), None
- if cmd == ('sudo', 'iptables-save', '-t', 'filter'):
+ if cmd == ('iptables-save', '-t', 'filter'):
return '\n'.join(self.in_filter_rules), None
- if cmd == ('sudo', 'iptables-save', '-t', 'nat'):
+ if cmd == ('iptables-save', '-t', 'nat'):
return '\n'.join(self.in_nat_rules), None
- if cmd == ('sudo', 'iptables-restore'):
+ if cmd == ('iptables-restore',):
lines = process_input.split('\n')
if '*filter' in lines:
self.out_rules = lines
return '', ''
- if cmd == ('sudo', 'ip6tables-restore'):
+ if cmd == ('ip6tables-restore',):
lines = process_input.split('\n')
if '*filter' in lines:
self.out6_rules = lines
@@ -1162,8 +1194,11 @@ class NWFilterTestCase(test.TestCase):
'project_id': 'fake',
'instance_type_id': 1})
- def _create_instance_type(self, params={}):
+ def _create_instance_type(self, params=None):
"""Create a test instance"""
+ if not params:
+ params = {}
+
context = self.context.elevated()
inst = {}
inst['name'] = 'm1.small'
diff --git a/nova/tests/test_metadata.py b/nova/tests/test_metadata.py
index c862726ab..ad678714e 100644
--- a/nova/tests/test_metadata.py
+++ b/nova/tests/test_metadata.py
@@ -43,16 +43,21 @@ class MetadataTestCase(test.TestCase):
'reservation_id': 'r-xxxxxxxx',
'user_data': '',
'image_ref': 7,
+ 'fixed_ips': [],
+ 'root_device_name': '/dev/sda1',
'hostname': 'test'})
def instance_get(*args, **kwargs):
return self.instance
+ def instance_get_list(*args, **kwargs):
+ return [self.instance]
+
def floating_get(*args, **kwargs):
return '99.99.99.99'
self.stubs.Set(api, 'instance_get', instance_get)
- self.stubs.Set(api, 'fixed_ip_get_instance', instance_get)
+ self.stubs.Set(api, 'instance_get_all_by_filters', instance_get_list)
self.stubs.Set(api, 'instance_get_floating_address', floating_get)
self.app = metadatarequesthandler.MetadataRequestHandler()
diff --git a/nova/tests/test_network.py b/nova/tests/test_network.py
index 28f50d328..2ca8b64f4 100644
--- a/nova/tests/test_network.py
+++ b/nova/tests/test_network.py
@@ -17,7 +17,6 @@
from nova import db
from nova import exception
-from nova import flags
from nova import log as logging
from nova import test
from nova.network import manager as network_manager
@@ -26,7 +25,6 @@ from nova.network import manager as network_manager
import mox
-FLAGS = flags.FLAGS
LOG = logging.getLogger('nova.tests.network')
diff --git a/nova/tests/test_nova_manage.py b/nova/tests/test_nova_manage.py
new file mode 100644
index 000000000..9c6563f14
--- /dev/null
+++ b/nova/tests/test_nova_manage.py
@@ -0,0 +1,82 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2011 OpenStack LLC
+# Copyright 2011 Ilya Alekseyev
+#
+# 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
+import sys
+
+TOPDIR = os.path.normpath(os.path.join(
+ os.path.dirname(os.path.abspath(__file__)),
+ os.pardir,
+ os.pardir))
+NOVA_MANAGE_PATH = os.path.join(TOPDIR, 'bin', 'nova-manage')
+
+sys.dont_write_bytecode = True
+import imp
+nova_manage = imp.load_source('nova_manage.py', NOVA_MANAGE_PATH)
+sys.dont_write_bytecode = False
+
+import netaddr
+from nova import context
+from nova import db
+from nova import flags
+from nova import test
+
+FLAGS = flags.FLAGS
+
+
+class FixedIpCommandsTestCase(test.TestCase):
+ def setUp(self):
+ super(FixedIpCommandsTestCase, self).setUp()
+ cidr = '10.0.0.0/24'
+ net = netaddr.IPNetwork(cidr)
+ net_info = {'bridge': 'fakebr',
+ 'bridge_interface': 'fakeeth',
+ 'dns': FLAGS.flat_network_dns,
+ 'cidr': cidr,
+ 'netmask': str(net.netmask),
+ 'gateway': str(net[1]),
+ 'broadcast': str(net.broadcast),
+ 'dhcp_start': str(net[2])}
+ self.network = db.network_create_safe(context.get_admin_context(),
+ net_info)
+ num_ips = len(net)
+ for index in range(num_ips):
+ address = str(net[index])
+ reserved = (index == 1 or index == 2)
+ db.fixed_ip_create(context.get_admin_context(),
+ {'network_id': self.network['id'],
+ 'address': address,
+ 'reserved': reserved})
+ self.commands = nova_manage.FixedIpCommands()
+
+ def tearDown(self):
+ db.network_delete_safe(context.get_admin_context(), self.network['id'])
+ super(FixedIpCommandsTestCase, self).tearDown()
+
+ def test_reserve(self):
+ self.commands.reserve('10.0.0.100')
+ address = db.fixed_ip_get_by_address(context.get_admin_context(),
+ '10.0.0.100')
+ self.assertEqual(address['reserved'], True)
+
+ def test_unreserve(self):
+ db.fixed_ip_update(context.get_admin_context(), '10.0.0.100',
+ {'reserved': True})
+ self.commands.unreserve('10.0.0.100')
+ address = db.fixed_ip_get_by_address(context.get_admin_context(),
+ '10.0.0.100')
+ self.assertEqual(address['reserved'], False)
diff --git a/nova/tests/test_quota.py b/nova/tests/test_quota.py
index 92393b536..f4b481ebe 100644
--- a/nova/tests/test_quota.py
+++ b/nova/tests/test_quota.py
@@ -114,9 +114,7 @@ class QuotaTestCase(test.TestCase):
db.quota_destroy_all_by_project(self.context, self.project_id)
def test_unlimited_instances(self):
- FLAGS.quota_instances = 2
- FLAGS.quota_ram = -1
- FLAGS.quota_cores = -1
+ self.flags(quota_instances=2, quota_ram=-1, quota_cores=-1)
instance_type = self._get_instance_type('m1.small')
num_instances = quota.allowed_instances(self.context, 100,
instance_type)
@@ -130,9 +128,7 @@ class QuotaTestCase(test.TestCase):
self.assertEqual(num_instances, 101)
def test_unlimited_ram(self):
- FLAGS.quota_instances = -1
- FLAGS.quota_ram = 2 * 2048
- FLAGS.quota_cores = -1
+ self.flags(quota_instances=-1, quota_ram=2 * 2048, quota_cores=-1)
instance_type = self._get_instance_type('m1.small')
num_instances = quota.allowed_instances(self.context, 100,
instance_type)
@@ -146,9 +142,7 @@ class QuotaTestCase(test.TestCase):
self.assertEqual(num_instances, 101)
def test_unlimited_cores(self):
- FLAGS.quota_instances = -1
- FLAGS.quota_ram = -1
- FLAGS.quota_cores = 2
+ self.flags(quota_instances=-1, quota_ram=-1, quota_cores=2)
instance_type = self._get_instance_type('m1.small')
num_instances = quota.allowed_instances(self.context, 100,
instance_type)
@@ -162,8 +156,7 @@ class QuotaTestCase(test.TestCase):
self.assertEqual(num_instances, 101)
def test_unlimited_volumes(self):
- FLAGS.quota_volumes = 10
- FLAGS.quota_gigabytes = -1
+ self.flags(quota_volumes=10, quota_gigabytes=-1)
volumes = quota.allowed_volumes(self.context, 100, 1)
self.assertEqual(volumes, 10)
db.quota_create(self.context, self.project_id, 'volumes', None)
@@ -173,8 +166,7 @@ class QuotaTestCase(test.TestCase):
self.assertEqual(volumes, 101)
def test_unlimited_gigabytes(self):
- FLAGS.quota_volumes = -1
- FLAGS.quota_gigabytes = 10
+ self.flags(quota_volumes=-1, quota_gigabytes=10)
volumes = quota.allowed_volumes(self.context, 100, 1)
self.assertEqual(volumes, 10)
db.quota_create(self.context, self.project_id, 'gigabytes', None)
@@ -184,7 +176,7 @@ class QuotaTestCase(test.TestCase):
self.assertEqual(volumes, 101)
def test_unlimited_floating_ips(self):
- FLAGS.quota_floating_ips = 10
+ self.flags(quota_floating_ips=10)
floating_ips = quota.allowed_floating_ips(self.context, 100)
self.assertEqual(floating_ips, 10)
db.quota_create(self.context, self.project_id, 'floating_ips', None)
@@ -194,7 +186,7 @@ class QuotaTestCase(test.TestCase):
self.assertEqual(floating_ips, 101)
def test_unlimited_metadata_items(self):
- FLAGS.quota_metadata_items = 10
+ self.flags(quota_metadata_items=10)
items = quota.allowed_metadata_items(self.context, 100)
self.assertEqual(items, 10)
db.quota_create(self.context, self.project_id, 'metadata_items', None)
@@ -286,49 +278,49 @@ class QuotaTestCase(test.TestCase):
metadata=metadata)
def test_default_allowed_injected_files(self):
- FLAGS.quota_max_injected_files = 55
+ self.flags(quota_max_injected_files=55)
self.assertEqual(quota.allowed_injected_files(self.context, 100), 55)
def test_overridden_allowed_injected_files(self):
- FLAGS.quota_max_injected_files = 5
+ self.flags(quota_max_injected_files=5)
db.quota_create(self.context, self.project_id, 'injected_files', 77)
self.assertEqual(quota.allowed_injected_files(self.context, 100), 77)
def test_unlimited_default_allowed_injected_files(self):
- FLAGS.quota_max_injected_files = -1
+ self.flags(quota_max_injected_files=-1)
self.assertEqual(quota.allowed_injected_files(self.context, 100), 100)
def test_unlimited_db_allowed_injected_files(self):
- FLAGS.quota_max_injected_files = 5
+ self.flags(quota_max_injected_files=5)
db.quota_create(self.context, self.project_id, 'injected_files', None)
self.assertEqual(quota.allowed_injected_files(self.context, 100), 100)
def test_default_allowed_injected_file_content_bytes(self):
- FLAGS.quota_max_injected_file_content_bytes = 12345
+ self.flags(quota_max_injected_file_content_bytes=12345)
limit = quota.allowed_injected_file_content_bytes(self.context, 23456)
self.assertEqual(limit, 12345)
def test_overridden_allowed_injected_file_content_bytes(self):
- FLAGS.quota_max_injected_file_content_bytes = 12345
+ self.flags(quota_max_injected_file_content_bytes=12345)
db.quota_create(self.context, self.project_id,
'injected_file_content_bytes', 5678)
limit = quota.allowed_injected_file_content_bytes(self.context, 23456)
self.assertEqual(limit, 5678)
def test_unlimited_default_allowed_injected_file_content_bytes(self):
- FLAGS.quota_max_injected_file_content_bytes = -1
+ self.flags(quota_max_injected_file_content_bytes=-1)
limit = quota.allowed_injected_file_content_bytes(self.context, 23456)
self.assertEqual(limit, 23456)
def test_unlimited_db_allowed_injected_file_content_bytes(self):
- FLAGS.quota_max_injected_file_content_bytes = 12345
+ self.flags(quota_max_injected_file_content_bytes=12345)
db.quota_create(self.context, self.project_id,
'injected_file_content_bytes', None)
limit = quota.allowed_injected_file_content_bytes(self.context, 23456)
self.assertEqual(limit, 23456)
def _create_with_injected_files(self, files):
- FLAGS.image_service = 'nova.image.fake.FakeImageService'
+ self.flags(image_service='nova.image.fake.FakeImageService')
api = compute.API(image_service=self.StubImageService())
inst_type = instance_types.get_instance_type_by_name('m1.small')
api.create(self.context, min_count=1, max_count=1,
@@ -336,7 +328,7 @@ class QuotaTestCase(test.TestCase):
injected_files=files)
def test_no_injected_files(self):
- FLAGS.image_service = 'nova.image.fake.FakeImageService'
+ self.flags(image_service='nova.image.fake.FakeImageService')
api = compute.API(image_service=self.StubImageService())
inst_type = instance_types.get_instance_type_by_name('m1.small')
api.create(self.context, instance_type=inst_type, image_href='3')
diff --git a/nova/tests/test_rpc.py b/nova/tests/test_rpc.py
index 2d2436175..ba9c0a859 100644
--- a/nova/tests/test_rpc.py
+++ b/nova/tests/test_rpc.py
@@ -20,13 +20,11 @@ Unit Tests for remote procedure calls using queue
"""
from nova import context
-from nova import flags
from nova import log as logging
from nova import rpc
from nova import test
-FLAGS = flags.FLAGS
LOG = logging.getLogger('nova.tests.rpc')
diff --git a/nova/tests/test_rpc_amqp.py b/nova/tests/test_rpc_amqp.py
index d29f7ae32..2215a908b 100644
--- a/nova/tests/test_rpc_amqp.py
+++ b/nova/tests/test_rpc_amqp.py
@@ -1,12 +1,32 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright (c) 2010 Openstack, LLC.
+# Administrator of the National Aeronautics and Space Administration.
+# 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.
+
+"""
+Tests For RPC AMQP.
+"""
+
from nova import context
-from nova import flags
from nova import log as logging
from nova import rpc
from nova.rpc import amqp
from nova import test
-FLAGS = flags.FLAGS
LOG = logging.getLogger('nova.tests.rpc')
diff --git a/nova/tests/test_service.py b/nova/tests/test_service.py
index bbf47b50f..8f92406ff 100644
--- a/nova/tests/test_service.py
+++ b/nova/tests/test_service.py
@@ -33,7 +33,6 @@ from nova import manager
from nova import wsgi
from nova.compute import manager as compute_manager
-FLAGS = flags.FLAGS
flags.DEFINE_string("fake_manager", "nova.tests.test_service.FakeManager",
"Manager for testing")
diff --git a/nova/tests/test_skip_examples.py b/nova/tests/test_skip_examples.py
new file mode 100644
index 000000000..8ca203442
--- /dev/null
+++ b/nova/tests/test_skip_examples.py
@@ -0,0 +1,47 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2010 United States Government as represented by the
+# Administrator of the National Aeronautics and Space Administration.
+# 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.
+
+from nova import test
+
+
+class ExampleSkipTestCase(test.TestCase):
+ test_counter = 0
+
+ @test.skip_test("Example usage of @test.skip_test()")
+ def test_skip_test_example(self):
+ self.fail("skip_test failed to work properly.")
+
+ @test.skip_if(True, "Example usage of @test.skip_if()")
+ def test_skip_if_example(self):
+ self.fail("skip_if failed to work properly.")
+
+ @test.skip_unless(False, "Example usage of @test.skip_unless()")
+ def test_skip_unless_example(self):
+ self.fail("skip_unless failed to work properly.")
+
+ @test.skip_if(False, "This test case should never be skipped.")
+ def test_001_increase_test_counter(self):
+ ExampleSkipTestCase.test_counter += 1
+
+ @test.skip_unless(True, "This test case should never be skipped.")
+ def test_002_increase_test_counter(self):
+ ExampleSkipTestCase.test_counter += 1
+
+ def test_003_verify_test_counter(self):
+ self.assertEquals(ExampleSkipTestCase.test_counter, 2,
+ "Tests were not skipped appropriately")
diff --git a/nova/tests/test_virt.py b/nova/tests/test_virt.py
new file mode 100644
index 000000000..388f075af
--- /dev/null
+++ b/nova/tests/test_virt.py
@@ -0,0 +1,83 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2011 Isaku Yamahata
+# 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.
+
+from nova import flags
+from nova import test
+from nova.virt import driver
+
+FLAGS = flags.FLAGS
+
+
+class TestVirtDriver(test.TestCase):
+ def test_block_device(self):
+ swap = {'device_name': '/dev/sdb',
+ 'swap_size': 1}
+ ephemerals = [{'num': 0,
+ 'virtual_name': 'ephemeral0',
+ 'device_name': '/dev/sdc1',
+ 'size': 1}]
+ block_device_mapping = [{'mount_device': '/dev/sde',
+ 'device_path': 'fake_device'}]
+ block_device_info = {
+ 'root_device_name': '/dev/sda',
+ 'swap': swap,
+ 'ephemerals': ephemerals,
+ 'block_device_mapping': block_device_mapping}
+
+ empty_block_device_info = {}
+
+ self.assertEqual(
+ driver.block_device_info_get_root(block_device_info), '/dev/sda')
+ self.assertEqual(
+ driver.block_device_info_get_root(empty_block_device_info), None)
+ self.assertEqual(
+ driver.block_device_info_get_root(None), None)
+
+ self.assertEqual(
+ driver.block_device_info_get_swap(block_device_info), swap)
+ self.assertEqual(driver.block_device_info_get_swap(
+ empty_block_device_info)['device_name'], None)
+ self.assertEqual(driver.block_device_info_get_swap(
+ empty_block_device_info)['swap_size'], 0)
+ self.assertEqual(
+ driver.block_device_info_get_swap({'swap': None})['device_name'],
+ None)
+ self.assertEqual(
+ driver.block_device_info_get_swap({'swap': None})['swap_size'],
+ 0)
+ self.assertEqual(
+ driver.block_device_info_get_swap(None)['device_name'], None)
+ self.assertEqual(
+ driver.block_device_info_get_swap(None)['swap_size'], 0)
+
+ self.assertEqual(
+ driver.block_device_info_get_ephemerals(block_device_info),
+ ephemerals)
+ self.assertEqual(
+ driver.block_device_info_get_ephemerals(empty_block_device_info),
+ [])
+ self.assertEqual(
+ driver.block_device_info_get_ephemerals(None),
+ [])
+
+ def test_swap_is_usable(self):
+ self.assertFalse(driver.swap_is_usable(None))
+ self.assertFalse(driver.swap_is_usable({'device_name': None}))
+ self.assertFalse(driver.swap_is_usable({'device_name': '/dev/sdb',
+ 'swap_size': 0}))
+ self.assertTrue(driver.swap_is_usable({'device_name': '/dev/sdb',
+ 'swap_size': 1}))
diff --git a/nova/tests/test_volume.py b/nova/tests/test_volume.py
index c0f89601f..7888b6b0b 100644
--- a/nova/tests/test_volume.py
+++ b/nova/tests/test_volume.py
@@ -414,8 +414,9 @@ class ISCSITestCase(DriverTestCase):
self.mox.StubOutWithMock(self.volume.driver, '_execute')
for i in volume_id_list:
tid = db.volume_get_iscsi_target_num(self.context, i)
- self.volume.driver._execute("sudo", "ietadm", "--op", "show",
- "--tid=%(tid)d" % locals())
+ self.volume.driver._execute("ietadm", "--op", "show",
+ "--tid=%(tid)d" % locals(),
+ run_as_root=True)
self.stream.truncate(0)
self.mox.ReplayAll()
@@ -433,8 +434,9 @@ class ISCSITestCase(DriverTestCase):
# the first vblade process isn't running
tid = db.volume_get_iscsi_target_num(self.context, volume_id_list[0])
self.mox.StubOutWithMock(self.volume.driver, '_execute')
- self.volume.driver._execute("sudo", "ietadm", "--op", "show",
- "--tid=%(tid)d" % locals()).AndRaise(
+ self.volume.driver._execute("ietadm", "--op", "show",
+ "--tid=%(tid)d" % locals(),
+ run_as_root=True).AndRaise(
exception.ProcessExecutionError())
self.mox.ReplayAll()
diff --git a/nova/tests/test_xenapi.py b/nova/tests/test_xenapi.py
index a795b3c74..1deb5a780 100644
--- a/nova/tests/test_xenapi.py
+++ b/nova/tests/test_xenapi.py
@@ -71,9 +71,9 @@ class XenAPIVolumeTestCase(test.TestCase):
self.user_id = 'fake'
self.project_id = 'fake'
self.context = context.RequestContext(self.user_id, self.project_id)
- FLAGS.target_host = '127.0.0.1'
- FLAGS.xenapi_connection_url = 'test_url'
- FLAGS.xenapi_connection_password = 'test_pass'
+ self.flags(target_host='127.0.0.1',
+ xenapi_connection_url='test_url',
+ xenapi_connection_password='test_pass')
db_fakes.stub_out_db_instance_api(self.stubs)
stubs.stub_out_get_target(self.stubs)
xenapi_fake.reset()
@@ -170,6 +170,10 @@ def reset_network(*args):
pass
+def _find_rescue_vbd_ref(*args):
+ pass
+
+
class XenAPIVMTestCase(test.TestCase):
"""Unit tests for VM operations."""
def setUp(self):
@@ -189,6 +193,8 @@ class XenAPIVMTestCase(test.TestCase):
stubs.stubout_stream_disk(self.stubs)
stubs.stubout_is_vdi_pv(self.stubs)
self.stubs.Set(vmops.VMOps, 'reset_network', reset_network)
+ self.stubs.Set(vmops.VMOps, '_find_rescue_vbd_ref',
+ _find_rescue_vbd_ref)
stubs.stub_out_vm_methods(self.stubs)
glance_stubs.stubout_glance_client(self.stubs)
fake_utils.stub_out_utils_execute(self.stubs)
@@ -397,7 +403,7 @@ class XenAPIVMTestCase(test.TestCase):
instance_type_id="3", os_type="linux",
architecture="x86-64", instance_id=1,
check_injection=False,
- create_record=True):
+ create_record=True, empty_dns=False):
stubs.stubout_loopingcall_start(self.stubs)
if create_record:
values = {'id': instance_id,
@@ -426,12 +432,22 @@ class XenAPIVMTestCase(test.TestCase):
'label': 'fake',
'mac': 'DE:AD:BE:EF:00:00',
'rxtx_cap': 3})]
+ if empty_dns:
+ network_info[0][1]['dns'] = []
+
self.conn.spawn(self.context, instance, network_info)
self.create_vm_record(self.conn, os_type, instance_id)
self.check_vm_record(self.conn, check_injection)
self.assertTrue(instance.os_type)
self.assertTrue(instance.architecture)
+ def test_spawn_empty_dns(self):
+ """"Test spawning with an empty dns list"""
+ self._test_spawn(glance_stubs.FakeGlance.IMAGE_VHD, None, None,
+ os_type="linux", architecture="x86-64",
+ empty_dns=True)
+ self.check_vm_params_for_linux()
+
def test_spawn_not_enough_memory(self):
self.assertRaises(Exception,
self._test_spawn,
@@ -532,8 +548,8 @@ class XenAPIVMTestCase(test.TestCase):
return '', ''
fake_utils.fake_execute_set_repliers([
- # Capture the sudo tee .../etc/network/interfaces command
- (r'(sudo\s+)?tee.*interfaces', _tee_handler),
+ # Capture the tee .../etc/network/interfaces command
+ (r'tee.*interfaces', _tee_handler),
])
self._test_spawn(glance_stubs.FakeGlance.IMAGE_MACHINE,
glance_stubs.FakeGlance.IMAGE_KERNEL,
@@ -576,9 +592,9 @@ class XenAPIVMTestCase(test.TestCase):
return '', ''
fake_utils.fake_execute_set_repliers([
- (r'(sudo\s+)?mount', _mount_handler),
- (r'(sudo\s+)?umount', _umount_handler),
- (r'(sudo\s+)?tee.*interfaces', _tee_handler)])
+ (r'mount', _mount_handler),
+ (r'umount', _umount_handler),
+ (r'tee.*interfaces', _tee_handler)])
self._test_spawn(1, 2, 3, check_injection=True)
# tee must not run in this case, where an injection-capable
@@ -638,6 +654,24 @@ class XenAPIVMTestCase(test.TestCase):
# Ensure that it will not unrescue a non-rescued instance.
self.assertRaises(Exception, conn.unrescue, instance, None)
+ def test_revert_migration(self):
+ instance = self._create_instance()
+
+ class VMOpsMock():
+
+ def __init__(self):
+ self.revert_migration_called = False
+
+ def revert_migration(self, instance):
+ self.revert_migration_called = True
+
+ stubs.stubout_session(self.stubs, stubs.FakeSessionForMigrationTests)
+
+ conn = xenapi_conn.get_connection(False)
+ conn._vmops = VMOpsMock()
+ conn.revert_migration(instance)
+ self.assertTrue(conn._vmops.revert_migration_called)
+
def _create_instance(self, instance_id=1, spawn=True):
"""Creates and spawns a test instance."""
stubs.stubout_loopingcall_start(self.stubs)
@@ -719,9 +753,9 @@ class XenAPIMigrateInstance(test.TestCase):
def setUp(self):
super(XenAPIMigrateInstance, self).setUp()
self.stubs = stubout.StubOutForTesting()
- FLAGS.target_host = '127.0.0.1'
- FLAGS.xenapi_connection_url = 'test_url'
- FLAGS.xenapi_connection_password = 'test_pass'
+ self.flags(target_host='127.0.0.1',
+ xenapi_connection_url='test_url',
+ xenapi_connection_password='test_pass')
db_fakes.stub_out_db_instance_api(self.stubs)
stubs.stub_out_get_target(self.stubs)
xenapi_fake.reset()
@@ -751,15 +785,67 @@ class XenAPIMigrateInstance(test.TestCase):
conn = xenapi_conn.get_connection(False)
conn.migrate_disk_and_power_off(instance, '127.0.0.1')
+ def test_revert_migrate(self):
+ instance = db.instance_create(self.context, self.values)
+ self.called = False
+ self.fake_vm_start_called = False
+ self.fake_revert_migration_called = False
+
+ def fake_vm_start(*args, **kwargs):
+ self.fake_vm_start_called = True
+
+ def fake_vdi_resize(*args, **kwargs):
+ self.called = True
+
+ def fake_revert_migration(*args, **kwargs):
+ self.fake_revert_migration_called = True
+
+ self.stubs.Set(stubs.FakeSessionForMigrationTests,
+ "VDI_resize_online", fake_vdi_resize)
+ self.stubs.Set(vmops.VMOps, '_start', fake_vm_start)
+ self.stubs.Set(vmops.VMOps, 'revert_migration', fake_revert_migration)
+
+ stubs.stubout_session(self.stubs, stubs.FakeSessionForMigrationTests)
+ stubs.stubout_loopingcall_start(self.stubs)
+ conn = xenapi_conn.get_connection(False)
+ network_info = [({'bridge': 'fa0', 'id': 0, 'injected': False},
+ {'broadcast': '192.168.0.255',
+ 'dns': ['192.168.0.1'],
+ 'gateway': '192.168.0.1',
+ 'gateway6': 'dead:beef::1',
+ 'ip6s': [{'enabled': '1',
+ 'ip': 'dead:beef::dcad:beff:feef:0',
+ 'netmask': '64'}],
+ 'ips': [{'enabled': '1',
+ 'ip': '192.168.0.100',
+ 'netmask': '255.255.255.0'}],
+ 'label': 'fake',
+ 'mac': 'DE:AD:BE:EF:00:00',
+ 'rxtx_cap': 3})]
+ conn.finish_migration(self.context, instance,
+ dict(base_copy='hurr', cow='durr'),
+ network_info, resize_instance=True)
+ self.assertEqual(self.called, True)
+ self.assertEqual(self.fake_vm_start_called, True)
+
+ conn.revert_migration(instance)
+ self.assertEqual(self.fake_revert_migration_called, True)
+
def test_finish_migrate(self):
instance = db.instance_create(self.context, self.values)
self.called = False
+ self.fake_vm_start_called = False
+
+ def fake_vm_start(*args, **kwargs):
+ self.fake_vm_start_called = True
def fake_vdi_resize(*args, **kwargs):
self.called = True
self.stubs.Set(stubs.FakeSessionForMigrationTests,
"VDI_resize_online", fake_vdi_resize)
+ self.stubs.Set(vmops.VMOps, '_start', fake_vm_start)
+
stubs.stubout_session(self.stubs, stubs.FakeSessionForMigrationTests)
stubs.stubout_loopingcall_start(self.stubs)
conn = xenapi_conn.get_connection(False)
@@ -781,6 +867,7 @@ class XenAPIMigrateInstance(test.TestCase):
dict(base_copy='hurr', cow='durr'),
network_info, resize_instance=True)
self.assertEqual(self.called, True)
+ self.assertEqual(self.fake_vm_start_called, True)
def test_finish_migrate_no_local_storage(self):
tiny_type_id = \
diff --git a/nova/tests/test_zones.py b/nova/tests/test_zones.py
index a943fee27..9efa23015 100644
--- a/nova/tests/test_zones.py
+++ b/nova/tests/test_zones.py
@@ -18,7 +18,6 @@ Tests For ZoneManager
import datetime
import mox
-import novaclient
from nova import context
from nova import db
diff --git a/nova/utils.py b/nova/utils.py
index 8e2683863..46d60c735 100644
--- a/nova/utils.py
+++ b/nova/utils.py
@@ -28,6 +28,7 @@ import netaddr
import os
import random
import re
+import shlex
import socket
import struct
import sys
@@ -126,29 +127,47 @@ def fetchfile(url, target):
def execute(*cmd, **kwargs):
+ """
+ Helper method to execute command with optional retry.
+
+ :cmd Passed to subprocess.Popen.
+ :process_input Send to opened process.
+ :check_exit_code Defaults to 0. Raise exception.ProcessExecutionError
+ unless program exits with this code.
+ :delay_on_retry True | False. Defaults to True. If set to True, wait a
+ short amount of time before retrying.
+ :attempts How many times to retry cmd.
+ :run_as_root True | False. Defaults to False. If set to True,
+ the command is prefixed by the command specified
+ in the root_helper FLAG.
+
+ :raises exception.Error on receiving unknown arguments
+ :raises exception.ProcessExecutionError
+ """
+
process_input = kwargs.pop('process_input', None)
- addl_env = kwargs.pop('addl_env', None)
check_exit_code = kwargs.pop('check_exit_code', 0)
delay_on_retry = kwargs.pop('delay_on_retry', True)
attempts = kwargs.pop('attempts', 1)
+ run_as_root = kwargs.pop('run_as_root', False)
if len(kwargs):
raise exception.Error(_('Got unknown keyword args '
'to utils.execute: %r') % kwargs)
+
+ if run_as_root:
+ cmd = shlex.split(FLAGS.root_helper) + list(cmd)
cmd = map(str, cmd)
while attempts > 0:
attempts -= 1
try:
LOG.debug(_('Running cmd (subprocess): %s'), ' '.join(cmd))
- env = os.environ.copy()
- if addl_env:
- env.update(addl_env)
_PIPE = subprocess.PIPE # pylint: disable=E1101
obj = subprocess.Popen(cmd,
stdin=_PIPE,
stdout=_PIPE,
stderr=_PIPE,
- env=env)
+ close_fds=True)
result = None
if process_input is not None:
result = obj.communicate(process_input)
@@ -223,7 +242,7 @@ def abspath(s):
def novadir():
import nova
- return os.path.abspath(nova.__file__).split('nova/__init__.pyc')[0]
+ return os.path.abspath(nova.__file__).split('nova/__init__.py')[0]
def default_flagfile(filename='nova.conf', args=None):
@@ -799,7 +818,7 @@ def parse_server_string(server_str):
(address, port) = server_str.split(':')
return (address, port)
- except:
+ except Exception:
LOG.debug(_('Invalid server_string: %s' % server_str))
return ('', '')
diff --git a/nova/virt/disk.py b/nova/virt/disk.py
index f8aea1f34..19f3ec185 100644
--- a/nova/virt/disk.py
+++ b/nova/virt/disk.py
@@ -73,7 +73,7 @@ def inject_data(image, key=None, net=None, partition=None, nbd=False):
try:
if not partition is None:
# create partition
- out, err = utils.execute('sudo', 'kpartx', '-a', device)
+ out, err = utils.execute('kpartx', '-a', device, run_as_root=True)
if err:
raise exception.Error(_('Failed to load partition: %s') % err)
mapped_device = '/dev/mapper/%sp%s' % (device.split('/')[-1],
@@ -90,14 +90,14 @@ def inject_data(image, key=None, net=None, partition=None, nbd=False):
mapped_device)
# Configure ext2fs so that it doesn't auto-check every N boots
- out, err = utils.execute('sudo', 'tune2fs',
- '-c', 0, '-i', 0, mapped_device)
+ out, err = utils.execute('tune2fs', '-c', 0, '-i', 0,
+ mapped_device, run_as_root=True)
tmpdir = tempfile.mkdtemp()
try:
# mount loopback to dir
- out, err = utils.execute(
- 'sudo', 'mount', mapped_device, tmpdir)
+ out, err = utils.execute('mount', mapped_device, tmpdir,
+ run_as_root=True)
if err:
raise exception.Error(_('Failed to mount filesystem: %s')
% err)
@@ -106,14 +106,14 @@ def inject_data(image, key=None, net=None, partition=None, nbd=False):
inject_data_into_fs(tmpdir, key, net, utils.execute)
finally:
# unmount device
- utils.execute('sudo', 'umount', mapped_device)
+ utils.execute('umount', mapped_device, run_as_root=True)
finally:
# remove temporary directory
utils.execute('rmdir', tmpdir)
finally:
if not partition is None:
# remove partitions
- utils.execute('sudo', 'kpartx', '-d', device)
+ utils.execute('kpartx', '-d', device, run_as_root=True)
finally:
_unlink_device(device, nbd)
@@ -128,7 +128,7 @@ def setup_container(image, container_dir=None, nbd=False):
"""
try:
device = _link_device(image, nbd)
- utils.execute('sudo', 'mount', device, container_dir)
+ utils.execute('mount', device, container_dir, run_as_root=True)
except Exception, exn:
LOG.exception(_('Failed to mount filesystem: %s'), exn)
_unlink_device(device, nbd)
@@ -144,9 +144,9 @@ def destroy_container(target, instance, nbd=False):
"""
try:
container_dir = '%s/rootfs' % target
- utils.execute('sudo', 'umount', container_dir)
+ utils.execute('umount', container_dir, run_as_root=True)
finally:
- out, err = utils.execute('sudo', 'losetup', '-a')
+ out, err = utils.execute('losetup', '-a', run_as_root=True)
for loop in out.splitlines():
if instance['name'] in loop:
device = loop.split(loop, ':')
@@ -157,7 +157,7 @@ def _link_device(image, nbd):
"""Link image to device using loopback or nbd"""
if nbd:
device = _allocate_device()
- utils.execute('sudo', 'qemu-nbd', '-c', device, image)
+ utils.execute('qemu-nbd', '-c', device, image, run_as_root=True)
# NOTE(vish): this forks into another process, so give it a chance
# to set up before continuuing
for i in xrange(FLAGS.timeout_nbd):
@@ -166,7 +166,8 @@ def _link_device(image, nbd):
time.sleep(1)
raise exception.Error(_('nbd device %s did not show up') % device)
else:
- out, err = utils.execute('sudo', 'losetup', '--find', '--show', image)
+ out, err = utils.execute('losetup', '--find', '--show', image,
+ run_as_root=True)
if err:
raise exception.Error(_('Could not attach image to loopback: %s')
% err)
@@ -176,10 +177,10 @@ def _link_device(image, nbd):
def _unlink_device(device, nbd):
"""Unlink image from device using loopback or nbd"""
if nbd:
- utils.execute('sudo', 'qemu-nbd', '-d', device)
+ utils.execute('qemu-nbd', '-d', device, run_as_root=True)
_free_device(device)
else:
- utils.execute('sudo', 'losetup', '--detach', device)
+ utils.execute('losetup', '--detach', device, run_as_root=True)
_DEVICES = ['/dev/nbd%s' % i for i in xrange(FLAGS.max_nbd_devices)]
@@ -220,12 +221,12 @@ def _inject_key_into_fs(key, fs, execute=None):
fs is the path to the base of the filesystem into which to inject the key.
"""
sshdir = os.path.join(fs, 'root', '.ssh')
- utils.execute('sudo', 'mkdir', '-p', sshdir) # existing dir doesn't matter
- utils.execute('sudo', 'chown', 'root', sshdir)
- utils.execute('sudo', 'chmod', '700', sshdir)
+ utils.execute('mkdir', '-p', sshdir, run_as_root=True)
+ utils.execute('chown', 'root', sshdir, run_as_root=True)
+ utils.execute('chmod', '700', sshdir, run_as_root=True)
keyfile = os.path.join(sshdir, 'authorized_keys')
- utils.execute('sudo', 'tee', '-a', keyfile,
- process_input='\n' + key.strip() + '\n')
+ utils.execute('tee', '-a', keyfile,
+ process_input='\n' + key.strip() + '\n', run_as_root=True)
def _inject_net_into_fs(net, fs, execute=None):
@@ -234,8 +235,8 @@ def _inject_net_into_fs(net, fs, execute=None):
net is the contents of /etc/network/interfaces.
"""
netdir = os.path.join(os.path.join(fs, 'etc'), 'network')
- utils.execute('sudo', 'mkdir', '-p', netdir) # existing dir doesn't matter
- utils.execute('sudo', 'chown', 'root:root', netdir)
- utils.execute('sudo', 'chmod', 755, netdir)
+ utils.execute('mkdir', '-p', netdir, run_as_root=True)
+ utils.execute('chown', 'root:root', netdir, run_as_root=True)
+ utils.execute('chmod', 755, netdir, run_as_root=True)
netfile = os.path.join(netdir, 'interfaces')
- utils.execute('sudo', 'tee', netfile, process_input=net)
+ utils.execute('tee', netfile, process_input=net, run_as_root=True)
diff --git a/nova/virt/driver.py b/nova/virt/driver.py
index 4f3cfefad..df4a66ac2 100644
--- a/nova/virt/driver.py
+++ b/nova/virt/driver.py
@@ -32,6 +32,33 @@ class InstanceInfo(object):
self.state = state
+def block_device_info_get_root(block_device_info):
+ block_device_info = block_device_info or {}
+ return block_device_info.get('root_device_name')
+
+
+def block_device_info_get_swap(block_device_info):
+ block_device_info = block_device_info or {}
+ return block_device_info.get('swap') or {'device_name': None,
+ 'swap_size': 0}
+
+
+def swap_is_usable(swap):
+ return swap and swap['device_name'] and swap['swap_size'] > 0
+
+
+def block_device_info_get_ephemerals(block_device_info):
+ block_device_info = block_device_info or {}
+ ephemerals = block_device_info.get('ephemerals') or []
+ return ephemerals
+
+
+def block_device_info_get_mapping(block_device_info):
+ block_device_info = block_device_info or {}
+ block_device_mapping = block_device_info.get('block_device_mapping') or []
+ return block_device_mapping
+
+
class ComputeDriver(object):
"""Base class for compute drivers.
@@ -65,8 +92,8 @@ class ComputeDriver(object):
# TODO(Vek): Need to pass context in for access to auth_token
raise NotImplementedError()
- def spawn(self, context, instance, network_info,
- block_device_mapping=None):
+ def spawn(self, context, instance,
+ network_info=None, block_device_info=None):
"""Launch a VM for the specified instance"""
raise NotImplementedError()
@@ -282,6 +309,10 @@ class ComputeDriver(object):
# TODO(Vek): Need to pass context in for access to auth_token
raise NotImplementedError()
+ def host_power_action(self, host, action):
+ """Reboots, shuts down or powers up the host."""
+ raise NotImplementedError()
+
def set_host_enabled(self, host, enabled):
"""Sets the specified host's ability to accept new instances."""
# TODO(Vek): Need to pass context in for access to auth_token
diff --git a/nova/virt/fake.py b/nova/virt/fake.py
index 80abcc644..880702af1 100644
--- a/nova/virt/fake.py
+++ b/nova/virt/fake.py
@@ -129,8 +129,8 @@ class FakeConnection(driver.ComputeDriver):
info_list.append(self._map_to_instance_info(instance))
return info_list
- def spawn(self, context, instance, network_info,
- block_device_mapping=None):
+ def spawn(self, context, instance,
+ network_info=None, block_device_info=None):
"""
Create a new instance/VM/domain on the virtualization platform.
@@ -294,7 +294,7 @@ class FakeConnection(driver.ComputeDriver):
"""
pass
- def destroy(self, instance, network_info):
+ def destroy(self, instance, network_info, cleanup=True):
key = instance.name
if key in self.instances:
del self.instances[key]
@@ -512,6 +512,10 @@ class FakeConnection(driver.ComputeDriver):
"""Return fake Host Status of ram, disk, network."""
return self.host_status
+ def host_power_action(self, host, action):
+ """Reboots, shuts down or powers up the host."""
+ pass
+
def set_host_enabled(self, host, enabled):
"""Sets the specified host's ability to accept new instances."""
pass
diff --git a/nova/virt/hyperv.py b/nova/virt/hyperv.py
index 3428a7fc1..03a78db1f 100644
--- a/nova/virt/hyperv.py
+++ b/nova/virt/hyperv.py
@@ -138,8 +138,8 @@ class HyperVConnection(driver.ComputeDriver):
return instance_infos
- def spawn(self, context, instance, network_info,
- block_device_mapping=None):
+ def spawn(self, context, instance,
+ network_info=None, block_device_info=None):
""" Create a new VM and start it."""
vm = self._lookup(instance.name)
if vm is not None:
@@ -374,7 +374,7 @@ class HyperVConnection(driver.ComputeDriver):
raise exception.InstanceNotFound(instance_id=instance.id)
self._set_vm_state(instance.name, 'Reboot')
- def destroy(self, instance, network_info):
+ def destroy(self, instance, network_info, cleanup=True):
"""Destroy the VM. Also destroy the associated VHD disk files"""
LOG.debug(_("Got request to destroy vm %s"), instance.name)
vm = self._lookup(instance.name)
@@ -499,6 +499,10 @@ class HyperVConnection(driver.ComputeDriver):
"""See xenapi_conn.py implementation."""
pass
+ def host_power_action(self, host, action):
+ """Reboots, shuts down or powers up the host."""
+ pass
+
def set_host_enabled(self, host, enabled):
"""Sets the specified host's ability to accept new instances."""
pass
diff --git a/nova/virt/libvirt.xml.template b/nova/virt/libvirt.xml.template
index a75636390..210e2b0fb 100644
--- a/nova/virt/libvirt.xml.template
+++ b/nova/virt/libvirt.xml.template
@@ -3,24 +3,22 @@
<memory>${memory_kb}</memory>
<os>
#if $type == 'lxc'
- #set $disk_prefix = ''
#set $disk_bus = ''
<type>exe</type>
<init>/sbin/init</init>
#else if $type == 'uml'
- #set $disk_prefix = 'ubd'
#set $disk_bus = 'uml'
<type>uml</type>
<kernel>/usr/bin/linux</kernel>
- <root>/dev/ubda</root>
+ #set $root_device_name = $getVar('root_device_name', '/dev/ubda')
+ <root>${root_device_name}</root>
#else
#if $type == 'xen'
- #set $disk_prefix = 'sd'
#set $disk_bus = 'scsi'
<type>linux</type>
- <root>/dev/xvda</root>
+ #set $root_device_name = $getVar('root_device_name', '/dev/xvda')
+ <root>${root_device_name}</root>
#else
- #set $disk_prefix = 'vd'
#set $disk_bus = 'virtio'
<type>hvm</type>
#end if
@@ -33,7 +31,8 @@
#if $type == 'xen'
<cmdline>ro</cmdline>
#else
- <cmdline>root=/dev/vda console=ttyS0</cmdline>
+ #set $root_device_name = $getVar('root_device_name', '/dev/vda')
+ <cmdline>root=${root_device_name} console=ttyS0</cmdline>
#end if
#if $getVar('ramdisk', None)
<initrd>${ramdisk}</initrd>
@@ -71,16 +70,30 @@
<disk type='file'>
<driver type='${driver_type}'/>
<source file='${basepath}/disk'/>
- <target dev='${disk_prefix}a' bus='${disk_bus}'/>
+ <target dev='${root_device}' bus='${disk_bus}'/>
</disk>
#end if
- #if $getVar('local', False)
+ #if $getVar('local_device', False)
<disk type='file'>
<driver type='${driver_type}'/>
<source file='${basepath}/disk.local'/>
- <target dev='${disk_prefix}b' bus='${disk_bus}'/>
+ <target dev='${local_device}' bus='${disk_bus}'/>
</disk>
#end if
+ #for $eph in $ephemerals
+ <disk type='block'>
+ <driver type='${driver_type}'/>
+ <source dev='${basepath}/${eph.device_path}'/>
+ <target dev='${eph.device}' bus='${disk_bus}'/>
+ </disk>
+ #end for
+ #if $getVar('swap_device', False)
+ <disk type='file'>
+ <driver type='${driver_type}'/>
+ <source file='${basepath}/disk.swap'/>
+ <target dev='${swap_device}' bus='${disk_bus}'/>
+ </disk>
+ #end if
#for $vol in $volumes
<disk type='${vol.type}'>
<driver type='raw'/>
diff --git a/nova/virt/libvirt/connection.py b/nova/virt/libvirt/connection.py
index 0acf25d28..6d043577a 100644
--- a/nova/virt/libvirt/connection.py
+++ b/nova/virt/libvirt/connection.py
@@ -43,7 +43,6 @@ import os
import random
import re
import shutil
-import subprocess
import sys
import tempfile
import time
@@ -54,6 +53,7 @@ from xml.etree import ElementTree
from eventlet import greenthread
from eventlet import tpool
+from nova import block_device
from nova import context as nova_context
from nova import db
from nova import exception
@@ -121,8 +121,6 @@ flags.DEFINE_integer('live_migration_bandwidth', 0,
'Define live migration behavior')
flags.DEFINE_string('qemu_img', 'qemu-img',
'binary to use for qemu-img commands')
-flags.DEFINE_bool('start_guests_on_host_boot', False,
- 'Whether to restart guests when the host reboots')
flags.DEFINE_string('libvirt_vif_type', 'bridge',
'Type of VIF to create.')
flags.DEFINE_string('libvirt_vif_driver',
@@ -153,8 +151,8 @@ def _late_load_cheetah():
Template = t.Template
-def _strip_dev(mount_path):
- return re.sub(r'^/dev/', '', mount_path)
+def _get_eph_disk(ephemeral):
+ return 'disk.eph' + str(ephemeral['num'])
class LibvirtConnection(driver.ComputeDriver):
@@ -173,27 +171,8 @@ class LibvirtConnection(driver.ComputeDriver):
self.vif_driver = utils.import_object(FLAGS.libvirt_vif_driver)
def init_host(self, host):
- # Adopt existing VM's running here
- ctxt = nova_context.get_admin_context()
- for instance in db.instance_get_all_by_host(ctxt, host):
- try:
- LOG.debug(_('Checking state of %s'), instance['name'])
- state = self.get_info(instance['name'])['state']
- except exception.NotFound:
- state = power_state.SHUTOFF
-
- LOG.debug(_('Current state of %(name)s was %(state)s.'),
- {'name': instance['name'], 'state': state})
- db.instance_set_state(ctxt, instance['id'], state)
-
- # NOTE(justinsb): We no longer delete SHUTOFF instances,
- # the user may want to power them back on
-
- if state != power_state.RUNNING:
- continue
- self.firewall_driver.setup_basic_filtering(instance)
- self.firewall_driver.prepare_instance_filter(instance)
- self.firewall_driver.apply_instance_filter(instance)
+ # NOTE(nsokolov): moved instance restarting to ComputeManager
+ pass
def _get_connection(self):
if not self._wrapped_conn or not self._test_connection():
@@ -370,7 +349,7 @@ class LibvirtConnection(driver.ComputeDriver):
"""Returns the xml for the disk mounted at device"""
try:
doc = libxml2.parseDoc(xml)
- except:
+ except Exception:
return None
ctx = doc.xpathNewContext()
try:
@@ -413,9 +392,7 @@ class LibvirtConnection(driver.ComputeDriver):
nova.image.get_image_service(image_href)
snapshot = snapshot_image_service.show(context, snapshot_image_id)
- metadata = {'disk_format': base['disk_format'],
- 'container_format': base['container_format'],
- 'is_public': False,
+ metadata = {'is_public': False,
'status': 'active',
'name': snapshot['name'],
'properties': {
@@ -430,6 +407,12 @@ class LibvirtConnection(driver.ComputeDriver):
arch = base['properties']['architecture']
metadata['properties']['architecture'] = arch
+ if 'disk_format' in base:
+ metadata['disk_format'] = base['disk_format']
+
+ if 'container_format' in base:
+ metadata['container_format'] = base['container_format']
+
# Make the snapshot
snapshot_name = uuid.uuid4().hex
snapshot_xml = """
@@ -591,24 +574,18 @@ class LibvirtConnection(driver.ComputeDriver):
# NOTE(ilyaalekseyev): Implementation like in multinics
# for xenapi(tr3buchet)
@exception.wrap_exception()
- def spawn(self, context, instance, network_info,
- block_device_mapping=None):
+ def spawn(self, context, instance,
+ network_info=None, block_device_info=None):
xml = self.to_xml(instance, False, network_info=network_info,
- block_device_mapping=block_device_mapping)
- block_device_mapping = block_device_mapping or []
+ block_device_info=block_device_info)
self.firewall_driver.setup_basic_filtering(instance, network_info)
self.firewall_driver.prepare_instance_filter(instance, network_info)
self._create_image(context, instance, xml, network_info=network_info,
- block_device_mapping=block_device_mapping)
+ block_device_info=block_device_info)
domain = self._create_new_domain(xml)
LOG.debug(_("instance %s: is running"), instance['name'])
self.firewall_driver.apply_instance_filter(instance)
- if FLAGS.start_guests_on_host_boot:
- LOG.debug(_("instance %s: setting autostart ON") %
- instance['name'])
- domain.setAutostart(1)
-
def _wait_for_boot():
"""Called at an interval until the VM is running."""
instance_name = instance['name']
@@ -634,9 +611,10 @@ class LibvirtConnection(driver.ComputeDriver):
if virsh_output.startswith('/dev/'):
LOG.info(_("cool, it's a device"))
- out, err = utils.execute('sudo', 'dd',
+ out, err = utils.execute('dd',
"if=%s" % virsh_output,
'iflag=nonblock',
+ run_as_root=True,
check_exit_code=False)
return out
else:
@@ -659,7 +637,7 @@ class LibvirtConnection(driver.ComputeDriver):
console_log = os.path.join(FLAGS.instances_path, instance['name'],
'console.log')
- utils.execute('sudo', 'chown', os.getuid(), console_log)
+ utils.execute('chown', os.getuid(), console_log, run_as_root=True)
if FLAGS.libvirt_type == 'xen':
# Xen is special
@@ -707,10 +685,10 @@ class LibvirtConnection(driver.ComputeDriver):
ajaxterm_cmd = 'sudo socat - %s' \
% get_pty_for_instance(instance['name'])
- cmd = '%s/tools/ajaxterm/ajaxterm.py --command "%s" -t %s -p %s' \
- % (utils.novadir(), ajaxterm_cmd, token, port)
+ cmd = ['%s/tools/ajaxterm/ajaxterm.py' % utils.novadir(),
+ '--command', ajaxterm_cmd, '-t', token, '-p', port]
- subprocess.Popen(cmd, shell=True)
+ utils.execute(cmd)
return {'token': token, 'host': host, 'port': port}
def get_host_ip_addr(self):
@@ -781,11 +759,14 @@ class LibvirtConnection(driver.ComputeDriver):
utils.execute('truncate', target, '-s', "%dG" % local_gb)
# TODO(vish): should we format disk by default?
+ def _create_swap(self, target, swap_gb):
+ """Create a swap file of specified size"""
+ self._create_local(target, swap_gb)
+ utils.execute('mkswap', target)
+
def _create_image(self, context, inst, libvirt_xml, suffix='',
disk_images=None, network_info=None,
- block_device_mapping=None):
- block_device_mapping = block_device_mapping or []
-
+ block_device_info=None):
if not suffix:
suffix = ''
@@ -844,8 +825,8 @@ class LibvirtConnection(driver.ComputeDriver):
size = None
root_fname += "_sm"
- if not self._volume_in_mapping(self.root_mount_device,
- block_device_mapping):
+ if not self._volume_in_mapping(self.default_root_device,
+ block_device_info):
self._cache_image(fn=self._fetch_image,
context=context,
target=basepath('disk'),
@@ -856,13 +837,38 @@ class LibvirtConnection(driver.ComputeDriver):
project_id=inst['project_id'],
size=size)
- if inst_type['local_gb'] and not self._volume_in_mapping(
- self.local_mount_device, block_device_mapping):
+ local_gb = inst['local_gb']
+ if local_gb and not self._volume_in_mapping(
+ self.default_local_device, block_device_info):
self._cache_image(fn=self._create_local,
target=basepath('disk.local'),
- fname="local_%s" % inst_type['local_gb'],
+ fname="local_%s" % local_gb,
+ cow=FLAGS.use_cow_images,
+ local_gb=local_gb)
+
+ for eph in driver.block_device_info_get_ephemerals(block_device_info):
+ self._cache_image(fn=self._create_local,
+ target=basepath(_get_eph_disk(eph)),
+ fname="local_%s" % eph['size'],
cow=FLAGS.use_cow_images,
- local_gb=inst_type['local_gb'])
+ local_gb=eph['size'])
+
+ swap_gb = 0
+
+ swap = driver.block_device_info_get_swap(block_device_info)
+ if driver.swap_is_usable(swap):
+ swap_gb = swap['swap_size']
+ elif (inst_type['swap'] > 0 and
+ not self._volume_in_mapping(self.default_swap_device,
+ block_device_info)):
+ swap_gb = inst_type['swap']
+
+ if swap_gb > 0:
+ self._cache_image(fn=self._create_swap,
+ target=basepath('disk.swap'),
+ fname="swap_%s" % swap_gb,
+ cow=FLAGS.use_cow_images,
+ swap_gb=swap_gb)
# For now, we assume that if we're not using a kernel, we're using a
# partitioned disk image where the target partition is the first
@@ -906,7 +912,7 @@ class LibvirtConnection(driver.ComputeDriver):
'netmask': netmask,
'gateway': mapping['gateway'],
'broadcast': mapping['broadcast'],
- 'dns': mapping['dns'],
+ 'dns': ' '.join(mapping['dns']),
'address_v6': address_v6,
'gateway6': gateway_v6,
'netmask_v6': netmask_v6}
@@ -941,18 +947,37 @@ class LibvirtConnection(driver.ComputeDriver):
' data into image %(img_id)s (%(e)s)') % locals())
if FLAGS.libvirt_type == 'uml':
- utils.execute('sudo', 'chown', 'root', basepath('disk'))
-
- root_mount_device = 'vda' # FIXME for now. it's hard coded.
- local_mount_device = 'vdb' # FIXME for now. it's hard coded.
-
- def _volume_in_mapping(self, mount_device, block_device_mapping):
- mount_device_ = _strip_dev(mount_device)
- for vol in block_device_mapping:
- vol_mount_device = _strip_dev(vol['mount_device'])
- if vol_mount_device == mount_device_:
- return True
- return False
+ utils.execute('chown', 'root', basepath('disk'), run_as_root=True)
+
+ if FLAGS.libvirt_type == 'uml':
+ _disk_prefix = 'ubd'
+ elif FLAGS.libvirt_type == 'xen':
+ _disk_prefix = 'sd'
+ elif FLAGS.libvirt_type == 'lxc':
+ _disk_prefix = ''
+ else:
+ _disk_prefix = 'vd'
+
+ default_root_device = _disk_prefix + 'a'
+ default_local_device = _disk_prefix + 'b'
+ default_swap_device = _disk_prefix + 'c'
+
+ def _volume_in_mapping(self, mount_device, block_device_info):
+ block_device_list = [block_device.strip_dev(vol['mount_device'])
+ for vol in
+ driver.block_device_info_get_mapping(
+ block_device_info)]
+ swap = driver.block_device_info_get_swap(block_device_info)
+ if driver.swap_is_usable(swap):
+ block_device_list.append(
+ block_device.strip_dev(swap['device_name']))
+ block_device_list += [block_device.strip_dev(ephemeral['device_name'])
+ for ephemeral in
+ driver.block_device_info_get_ephemerals(
+ block_device_info)]
+
+ LOG.debug(_("block_device_list %s"), block_device_list)
+ return block_device.strip_dev(mount_device) in block_device_list
def _get_volume_device_info(self, device_path):
if device_path.startswith('/dev/'):
@@ -964,8 +989,9 @@ class LibvirtConnection(driver.ComputeDriver):
raise exception.InvalidDevicePath(path=device_path)
def _prepare_xml_info(self, instance, rescue=False, network_info=None,
- block_device_mapping=None):
- block_device_mapping = block_device_mapping or []
+ block_device_info=None):
+ block_device_mapping = driver.block_device_info_get_mapping(
+ block_device_info)
# TODO(adiantum) remove network_info creation code
# when multinics will be completed
if not network_info:
@@ -984,17 +1010,27 @@ class LibvirtConnection(driver.ComputeDriver):
driver_type = 'raw'
for vol in block_device_mapping:
- vol['mount_device'] = _strip_dev(vol['mount_device'])
+ vol['mount_device'] = block_device.strip_dev(vol['mount_device'])
(vol['type'], vol['protocol'], vol['name']) = \
self._get_volume_device_info(vol['device_path'])
- ebs_root = self._volume_in_mapping(self.root_mount_device,
- block_device_mapping)
- if self._volume_in_mapping(self.local_mount_device,
- block_device_mapping):
- local_gb = False
- else:
- local_gb = inst_type['local_gb']
+ ebs_root = self._volume_in_mapping(self.default_root_device,
+ block_device_info)
+
+ local_device = False
+ if not (self._volume_in_mapping(self.default_local_device,
+ block_device_info) or
+ 0 in [eph['num'] for eph in
+ driver.block_device_info_get_ephemerals(
+ block_device_info)]):
+ if instance['local_gb'] > 0:
+ local_device = self.default_local_device
+
+ ephemerals = []
+ for eph in driver.block_device_info_get_ephemerals(block_device_info):
+ ephemerals.append({'device_path': _get_eph_disk(eph),
+ 'device': block_device.strip_dev(
+ eph['device_name'])})
xml_info = {'type': FLAGS.libvirt_type,
'name': instance['name'],
@@ -1003,12 +1039,35 @@ class LibvirtConnection(driver.ComputeDriver):
'memory_kb': inst_type['memory_mb'] * 1024,
'vcpus': inst_type['vcpus'],
'rescue': rescue,
- 'local': local_gb,
+ 'disk_prefix': self._disk_prefix,
'driver_type': driver_type,
'vif_type': FLAGS.libvirt_vif_type,
'nics': nics,
'ebs_root': ebs_root,
- 'volumes': block_device_mapping}
+ 'local_device': local_device,
+ 'volumes': block_device_mapping,
+ 'ephemerals': ephemerals}
+
+ root_device_name = driver.block_device_info_get_root(block_device_info)
+ if root_device_name:
+ xml_info['root_device'] = block_device.strip_dev(root_device_name)
+ xml_info['root_device_name'] = root_device_name
+ else:
+ # NOTE(yamahata):
+ # for nova.api.ec2.cloud.CloudController.get_metadata()
+ xml_info['root_device'] = self.default_root_device
+ db.instance_update(
+ nova_context.get_admin_context(), instance['id'],
+ {'root_device_name': '/dev/' + self.default_root_device})
+
+ swap = driver.block_device_info_get_swap(block_device_info)
+ if driver.swap_is_usable(swap):
+ xml_info['swap_device'] = block_device.strip_dev(
+ swap['device_name'])
+ elif (inst_type['swap'] > 0 and
+ not self._volume_in_mapping(self.default_swap_device,
+ block_device_info)):
+ xml_info['swap_device'] = self.default_swap_device
if FLAGS.vnc_enabled and FLAGS.libvirt_type not in ('lxc', 'uml'):
xml_info['vncserver_host'] = FLAGS.vncserver_host
@@ -1024,12 +1083,11 @@ class LibvirtConnection(driver.ComputeDriver):
return xml_info
def to_xml(self, instance, rescue=False, network_info=None,
- block_device_mapping=None):
- block_device_mapping = block_device_mapping or []
+ block_device_info=None):
# TODO(termie): cache?
LOG.debug(_('instance %s: starting toXML method'), instance['name'])
xml_info = self._prepare_xml_info(instance, rescue, network_info,
- block_device_mapping)
+ block_device_info)
xml = str(Template(self.libvirt_xml, searchList=[xml_info]))
LOG.debug(_('instance %s: finished toXML method'), instance['name'])
return xml
@@ -1103,7 +1161,7 @@ class LibvirtConnection(driver.ComputeDriver):
try:
doc = libxml2.parseDoc(xml)
- except:
+ except Exception:
return []
ctx = doc.xpathNewContext()
@@ -1144,7 +1202,7 @@ class LibvirtConnection(driver.ComputeDriver):
try:
doc = libxml2.parseDoc(xml)
- except:
+ except Exception:
return []
ctx = doc.xpathNewContext()
@@ -1584,6 +1642,10 @@ class LibvirtConnection(driver.ComputeDriver):
"""See xenapi_conn.py implementation."""
pass
+ def host_power_action(self, host, action):
+ """Reboots, shuts down or powers up the host."""
+ pass
+
def set_host_enabled(self, host, enabled):
"""Sets the specified host's ability to accept new instances."""
pass
diff --git a/nova/virt/libvirt/vif.py b/nova/virt/libvirt/vif.py
index f42eeec98..57f5aa87c 100644
--- a/nova/virt/libvirt/vif.py
+++ b/nova/virt/libvirt/vif.py
@@ -25,6 +25,7 @@ from nova.network import linux_net
from nova.virt.libvirt import netutils
from nova import utils
from nova.virt.vif import VIFDriver
+from nova import exception
LOG = logging.getLogger('nova.virt.libvirt.vif')
@@ -104,16 +105,18 @@ class LibvirtOpenVswitchDriver(VIFDriver):
dev = "tap-%s" % vif_id
iface_id = "nova-" + vif_id
if not linux_net._device_exists(dev):
- utils.execute('sudo', 'ip', 'tuntap', 'add', dev, 'mode', 'tap')
- utils.execute('sudo', 'ip', 'link', 'set', dev, 'up')
- utils.execute('sudo', 'ovs-vsctl', '--', '--may-exist', 'add-port',
+ utils.execute('ip', 'tuntap', 'add', dev, 'mode', 'tap',
+ run_as_root=True)
+ utils.execute('ip', 'link', 'set', dev, 'up', run_as_root=True)
+ utils.execute('ovs-vsctl', '--', '--may-exist', 'add-port',
FLAGS.libvirt_ovs_bridge, dev,
'--', 'set', 'Interface', dev,
"external-ids:iface-id=%s" % iface_id,
'--', 'set', 'Interface', dev,
"external-ids:iface-status=active",
'--', 'set', 'Interface', dev,
- "external-ids:attached-mac=%s" % mapping['mac'])
+ "external-ids:attached-mac=%s" % mapping['mac'],
+ run_as_root=True)
result = {
'script': '',
@@ -127,10 +130,10 @@ class LibvirtOpenVswitchDriver(VIFDriver):
vif_id = str(instance['id']) + "-" + str(network['id'])
dev = "tap-%s" % vif_id
try:
- utils.execute('sudo', 'ovs-vsctl', 'del-port',
- network['bridge'], dev)
- utils.execute('sudo', 'ip', 'link', 'delete', dev)
- except:
+ utils.execute('ovs-vsctl', 'del-port',
+ network['bridge'], dev, run_as_root=True)
+ utils.execute('ip', 'link', 'delete', dev, run_as_root=True)
+ except exception.ProcessExecutionError:
LOG.warning(_("Failed while unplugging vif of instance '%s'"),
instance['name'])
raise
diff --git a/nova/virt/vmwareapi/io_util.py b/nova/virt/vmwareapi/io_util.py
index 2ec773b7b..409242800 100644
--- a/nova/virt/vmwareapi/io_util.py
+++ b/nova/virt/vmwareapi/io_util.py
@@ -68,7 +68,10 @@ class GlanceWriteThread(object):
"""Ensures that image data is written to in the glance client and that
it is in correct ('active')state."""
- def __init__(self, input, glance_client, image_id, image_meta={}):
+ def __init__(self, input, glance_client, image_id, image_meta=None):
+ if not image_meta:
+ image_meta = {}
+
self.input = input
self.glance_client = glance_client
self.image_id = image_id
diff --git a/nova/virt/vmwareapi/vif.py b/nova/virt/vmwareapi/vif.py
index b3e43b209..fb6548b34 100644
--- a/nova/virt/vmwareapi/vif.py
+++ b/nova/virt/vmwareapi/vif.py
@@ -63,7 +63,7 @@ class VMWareVlanBridgeDriver(VIFDriver):
vswitch_associated = network_utils.get_vswitch_for_vlan_interface(
session, vlan_interface)
if vswitch_associated is None:
- raise exception.SwicthNotFoundForNetworkAdapter(
+ raise exception.SwitchNotFoundForNetworkAdapter(
adapter=vlan_interface)
# Check whether bridge already exists and retrieve the the ref of the
# network whose name_label is "bridge"
diff --git a/nova/virt/vmwareapi/vim_util.py b/nova/virt/vmwareapi/vim_util.py
index 11214231c..e03daddac 100644
--- a/nova/virt/vmwareapi/vim_util.py
+++ b/nova/virt/vmwareapi/vim_util.py
@@ -95,9 +95,12 @@ def build_recursive_traversal_spec(client_factory):
def build_property_spec(client_factory, type="VirtualMachine",
- properties_to_collect=["name"],
+ properties_to_collect=None,
all_properties=False):
"""Builds the Property Spec."""
+ if not properties_to_collect:
+ properties_to_collect = ["name"]
+
property_spec = client_factory.create('ns0:PropertySpec')
property_spec.all = all_properties
property_spec.pathSet = properties_to_collect
@@ -155,8 +158,11 @@ def get_dynamic_property(vim, mobj, type, property_name):
return property_value
-def get_objects(vim, type, properties_to_collect=["name"], all=False):
+def get_objects(vim, type, properties_to_collect=None, all=False):
"""Gets the list of objects of the type specified."""
+ if not properties_to_collect:
+ properties_to_collect = ["name"]
+
client_factory = vim.client.factory
object_spec = build_object_spec(client_factory,
vim.get_service_content().rootFolder,
diff --git a/nova/virt/vmwareapi/vmware_images.py b/nova/virt/vmwareapi/vmware_images.py
index 70adba74f..f5f75dae2 100644
--- a/nova/virt/vmwareapi/vmware_images.py
+++ b/nova/virt/vmwareapi/vmware_images.py
@@ -33,11 +33,15 @@ QUEUE_BUFFER_SIZE = 10
def start_transfer(read_file_handle, data_size, write_file_handle=None,
- glance_client=None, image_id=None, image_meta={}):
+ glance_client=None, image_id=None, image_meta=None):
"""Start the data transfer from the reader to the writer.
Reader writes to the pipe and the writer reads from the pipe. This means
that the total transfer time boils down to the slower of the read/write
and not the addition of the two times."""
+
+ if not image_meta:
+ image_meta = {}
+
# The pipe that acts as an intermediate store of data for reader to write
# to and writer to grab from.
thread_safe_pipe = io_util.ThreadSafePipe(QUEUE_BUFFER_SIZE, data_size)
diff --git a/nova/virt/vmwareapi_conn.py b/nova/virt/vmwareapi_conn.py
index 3d209fa99..243ee64f5 100644
--- a/nova/virt/vmwareapi_conn.py
+++ b/nova/virt/vmwareapi_conn.py
@@ -137,7 +137,7 @@ class VMWareESXConnection(driver.ComputeDriver):
"""Reboot VM instance."""
self._vmops.reboot(instance, network_info)
- def destroy(self, instance, network_info):
+ def destroy(self, instance, network_info, cleanup=True):
"""Destroy VM instance."""
self._vmops.destroy(instance, network_info)
@@ -191,6 +191,10 @@ class VMWareESXConnection(driver.ComputeDriver):
"""This method is supported only by libvirt."""
return
+ def host_power_action(self, host, action):
+ """Reboots, shuts down or powers up the host."""
+ pass
+
def set_host_enabled(self, host, enabled):
"""Sets the specified host's ability to accept new instances."""
pass
diff --git a/nova/virt/xenapi/vm_utils.py b/nova/virt/xenapi/vm_utils.py
index 63bc191cf..6c44d53d4 100644
--- a/nova/virt/xenapi/vm_utils.py
+++ b/nova/virt/xenapi/vm_utils.py
@@ -797,7 +797,7 @@ def get_vdi_for_vm_safely(session, vm_ref):
else:
num_vdis = len(vdi_refs)
if num_vdis != 1:
- raise exception.Exception(_("Unexpected number of VDIs"
+ raise exception.Error(_("Unexpected number of VDIs"
"(%(num_vdis)s) found"
" for VM %(vm_ref)s") % locals())
@@ -967,7 +967,7 @@ def _stream_disk(dev, image_type, virtual_size, image_file):
offset = MBR_SIZE_BYTES
_write_partition(virtual_size, dev)
- utils.execute('sudo', 'chown', os.getuid(), '/dev/%s' % dev)
+ utils.execute('chown', os.getuid(), '/dev/%s' % dev, run_as_root=True)
with open('/dev/%s' % dev, 'wb') as f:
f.seek(offset)
@@ -986,10 +986,11 @@ def _write_partition(virtual_size, dev):
def execute(*cmd, **kwargs):
return utils.execute(*cmd, **kwargs)
- execute('sudo', 'parted', '--script', dest, 'mklabel', 'msdos')
- execute('sudo', 'parted', '--script', dest, 'mkpart', 'primary',
+ execute('parted', '--script', dest, 'mklabel', 'msdos', run_as_root=True)
+ execute('parted', '--script', dest, 'mkpart', 'primary',
'%ds' % primary_first,
- '%ds' % primary_last)
+ '%ds' % primary_last,
+ run_as_root=True)
LOG.debug(_('Writing partition table %s done.'), dest)
@@ -1002,9 +1003,9 @@ def get_name_label_for_image(image):
def _mount_filesystem(dev_path, dir):
"""mounts the device specified by dev_path in dir"""
try:
- out, err = utils.execute('sudo', 'mount',
+ out, err = utils.execute('mount',
'-t', 'ext2,ext3',
- dev_path, dir)
+ dev_path, dir, run_as_root=True)
except exception.ProcessExecutionError as e:
err = str(e)
return err
@@ -1056,7 +1057,7 @@ def _mounted_processing(device, key, net):
disk.inject_data_into_fs(tmpdir, key, net,
utils.execute)
finally:
- utils.execute('sudo', 'umount', dev_path)
+ utils.execute('umount', dev_path, run_as_root=True)
else:
LOG.info(_('Failed to mount filesystem (expected for '
'non-linux instances): %s') % err)
@@ -1095,6 +1096,8 @@ def _prepare_injectables(inst, networks_info):
ip_v6 = info['ip6s'][0]
if len(info['dns']) > 0:
dns = info['dns'][0]
+ else:
+ dns = ''
interface_info = {'name': 'eth%d' % ifc_num,
'address': ip_v4 and ip_v4['ip'] or '',
'netmask': ip_v4 and ip_v4['netmask'] or '',
diff --git a/nova/virt/xenapi/vmops.py b/nova/virt/xenapi/vmops.py
index b3b812a48..b9cd59946 100644
--- a/nova/virt/xenapi/vmops.py
+++ b/nova/virt/xenapi/vmops.py
@@ -122,7 +122,7 @@ class VMOps(object):
network_info)
if resize_instance:
self.resize_instance(instance, vdi_uuid)
- self._spawn(instance, vm_ref)
+ self._start(instance, vm_ref=vm_ref)
def _start(self, instance, vm_ref=None):
"""Power on a VM instance"""
@@ -282,6 +282,7 @@ class VMOps(object):
'architecture': instance.architecture})
def _check_agent_version():
+ LOG.debug(_("Querying agent version"))
if instance.os_type == 'windows':
# Windows will generally perform a setup process on first boot
# that can take a couple of minutes and then reboot. So we
@@ -292,7 +293,6 @@ class VMOps(object):
else:
version = self.get_agent_version(instance)
if not version:
- LOG.info(_('No agent version returned by instance'))
return
LOG.info(_('Instance agent version: %s') % version)
@@ -327,6 +327,10 @@ class VMOps(object):
LOG.debug(_("Setting admin password"))
self.set_admin_password(instance, admin_password)
+ def _reset_network():
+ LOG.debug(_("Resetting network"))
+ self.reset_network(instance, vm_ref)
+
# NOTE(armando): Do we really need to do this in virt?
# NOTE(tr3buchet): not sure but wherever we do it, we need to call
# reset_network afterwards
@@ -341,7 +345,7 @@ class VMOps(object):
_check_agent_version()
_inject_files()
_set_admin_password()
- self.reset_network(instance, vm_ref)
+ _reset_network()
return True
except Exception, exc:
LOG.warn(exc)
@@ -597,13 +601,13 @@ class VMOps(object):
transaction_id = str(uuid.uuid4())
args = {'id': transaction_id}
resp = self._make_agent_call('version', instance, '', args)
- if resp is None:
- # No response from the agent
- return
- resp_dict = json.loads(resp)
+ if resp['returncode'] != '0':
+ LOG.error(_('Failed to query agent version: %(resp)r') %
+ locals())
+ return None
# Some old versions of the Windows agent have a trailing \\r\\n
# (ie CRLF escaped) for some reason. Strip that off.
- return resp_dict['message'].replace('\\r\\n', '')
+ return resp['message'].replace('\\r\\n', '')
if timeout:
vm_ref = self._get_vm_opaque_ref(instance)
@@ -634,13 +638,10 @@ class VMOps(object):
transaction_id = str(uuid.uuid4())
args = {'id': transaction_id, 'url': url, 'md5sum': md5sum}
resp = self._make_agent_call('agentupdate', instance, '', args)
- if resp is None:
- # No response from the agent
- return
- resp_dict = json.loads(resp)
- if resp_dict['returncode'] != '0':
- raise RuntimeError(resp_dict['message'])
- return resp_dict['message']
+ if resp['returncode'] != '0':
+ LOG.error(_('Failed to update agent: %(resp)r') % locals())
+ return None
+ return resp['message']
def set_admin_password(self, instance, new_pass):
"""Set the root/admin password on the VM instance.
@@ -659,18 +660,13 @@ class VMOps(object):
key_init_args = {'id': key_init_transaction_id,
'pub': str(dh.get_public())}
resp = self._make_agent_call('key_init', instance, '', key_init_args)
- if resp is None:
- # No response from the agent
- return
- resp_dict = json.loads(resp)
# Successful return code from key_init is 'D0'
- if resp_dict['returncode'] != 'D0':
- # There was some sort of error; the message will contain
- # a description of the error.
- raise RuntimeError(resp_dict['message'])
+ if resp['returncode'] != 'D0':
+ LOG.error(_('Failed to exchange keys: %(resp)r') % locals())
+ return None
# Some old versions of the Windows agent have a trailing \\r\\n
# (ie CRLF escaped) for some reason. Strip that off.
- agent_pub = int(resp_dict['message'].replace('\\r\\n', ''))
+ agent_pub = int(resp['message'].replace('\\r\\n', ''))
dh.compute_shared(agent_pub)
# Some old versions of Linux and Windows agent expect trailing \n
# on password to work correctly.
@@ -679,17 +675,14 @@ class VMOps(object):
password_transaction_id = str(uuid.uuid4())
password_args = {'id': password_transaction_id, 'enc_pass': enc_pass}
resp = self._make_agent_call('password', instance, '', password_args)
- if resp is None:
- # No response from the agent
- return
- resp_dict = json.loads(resp)
# Successful return code from password is '0'
- if resp_dict['returncode'] != '0':
- raise RuntimeError(resp_dict['message'])
+ if resp['returncode'] != '0':
+ LOG.error(_('Failed to update password: %(resp)r') % locals())
+ return None
db.instance_update(nova_context.get_admin_context(),
instance['id'],
dict(admin_pass=new_pass))
- return resp_dict['message']
+ return resp['message']
def inject_file(self, instance, path, contents):
"""Write a file to the VM instance.
@@ -712,12 +705,10 @@ class VMOps(object):
# If the agent doesn't support file injection, a NotImplementedError
# will be raised with the appropriate message.
resp = self._make_agent_call('inject_file', instance, '', args)
- resp_dict = json.loads(resp)
- if resp_dict['returncode'] != '0':
- # There was some other sort of error; the message will contain
- # a description of the error.
- raise RuntimeError(resp_dict['message'])
- return resp_dict['message']
+ if resp['returncode'] != '0':
+ LOG.error(_('Failed to inject file: %(resp)r') % locals())
+ return None
+ return resp['message']
def _shutdown(self, instance, vm_ref, hard=True):
"""Shutdown an instance."""
@@ -743,6 +734,17 @@ class VMOps(object):
except self.XenAPI.Failure, exc:
LOG.exception(exc)
+ def _find_rescue_vbd_ref(self, vm_ref, rescue_vm_ref):
+ """Find and return the rescue VM's vbd_ref.
+
+ We use the second VBD here because swap is first with the root file
+ system coming in second."""
+ vbd_ref = self._session.get_xenapi().VM.get_VBDs(vm_ref)[1]
+ vdi_ref = self._session.get_xenapi().VBD.get_record(vbd_ref)["VDI"]
+
+ return VMHelper.create_vbd(self._session, rescue_vm_ref, vdi_ref, 1,
+ False)
+
def _shutdown_rescue(self, rescue_vm_ref):
"""Shutdown a rescue instance."""
self._session.call_xenapi("Async.VM.hard_shutdown", rescue_vm_ref)
@@ -914,7 +916,7 @@ class VMOps(object):
True)
self._wait_with_callback(instance.id, task, callback)
- def rescue(self, context, instance, callback, network_info):
+ def rescue(self, context, instance, _callback, network_info):
"""Rescue the specified instance.
- shutdown the instance VM.
@@ -934,15 +936,11 @@ class VMOps(object):
instance._rescue = True
self.spawn_rescue(context, instance, network_info)
rescue_vm_ref = VMHelper.lookup(self._session, instance.name)
-
- vbd_ref = self._session.get_xenapi().VM.get_VBDs(vm_ref)[0]
- vdi_ref = self._session.get_xenapi().VBD.get_record(vbd_ref)["VDI"]
- rescue_vbd_ref = VMHelper.create_vbd(self._session, rescue_vm_ref,
- vdi_ref, 1, False)
+ rescue_vbd_ref = self._find_rescue_vbd_ref(vm_ref, rescue_vm_ref)
self._session.call_xenapi("Async.VBD.plug", rescue_vbd_ref)
- def unrescue(self, instance, callback):
+ def unrescue(self, instance, _callback):
"""Unrescue the specified instance.
- unplug the instance VM's disk from the rescue VM.
@@ -1024,11 +1022,23 @@ class VMOps(object):
# TODO: implement this!
return 'http://fakeajaxconsole/fake_url'
+ def host_power_action(self, host, action):
+ """Reboots or shuts down the host."""
+ args = {"action": json.dumps(action)}
+ methods = {"reboot": "host_reboot", "shutdown": "host_shutdown"}
+ json_resp = self._call_xenhost(methods[action], args)
+ resp = json.loads(json_resp)
+ return resp["power_action"]
+
def set_host_enabled(self, host, enabled):
"""Sets the specified host's ability to accept new instances."""
args = {"enabled": json.dumps(enabled)}
- json_resp = self._call_xenhost("set_host_enabled", args)
- resp = json.loads(json_resp)
+ xenapi_resp = self._call_xenhost("set_host_enabled", args)
+ try:
+ resp = json.loads(xenapi_resp)
+ except TypeError as e:
+ # Already logged; return the message
+ return xenapi_resp.details[-1]
return resp["status"]
def _call_xenhost(self, method, arg_dict):
@@ -1044,7 +1054,7 @@ class VMOps(object):
#args={"params": arg_dict})
ret = self._session.wait_for_task(task, task_id)
except self.XenAPI.Failure as e:
- ret = None
+ ret = e
LOG.error(_("The call to %(method)s returned an error: %(e)s.")
% locals())
return ret
@@ -1159,8 +1169,19 @@ class VMOps(object):
def _make_agent_call(self, method, vm, path, addl_args=None):
"""Abstracts out the interaction with the agent xenapi plugin."""
- return self._make_plugin_call('agent', method=method, vm=vm,
+ ret = self._make_plugin_call('agent', method=method, vm=vm,
path=path, addl_args=addl_args)
+ if isinstance(ret, dict):
+ return ret
+ try:
+ return json.loads(ret)
+ except TypeError:
+ instance_id = vm.id
+ LOG.error(_('The agent call to %(method)s returned an invalid'
+ ' response: %(ret)r. VM id=%(instance_id)s;'
+ ' path=%(path)s; args=%(addl_args)r') % locals())
+ return {'returncode': 'error',
+ 'message': 'unable to deserialize response'}
def _make_plugin_call(self, plugin, method, vm, path, addl_args=None,
vm_ref=None):
@@ -1178,20 +1199,20 @@ class VMOps(object):
ret = self._session.wait_for_task(task, instance_id)
except self.XenAPI.Failure, e:
ret = None
- err_trace = e.details[-1]
- err_msg = err_trace.splitlines()[-1]
- strargs = str(args)
+ err_msg = e.details[-1].splitlines()[-1]
if 'TIMEOUT:' in err_msg:
LOG.error(_('TIMEOUT: The call to %(method)s timed out. '
- 'VM id=%(instance_id)s; args=%(strargs)s') % locals())
+ 'VM id=%(instance_id)s; args=%(args)r') % locals())
+ return {'returncode': 'timeout', 'message': err_msg}
elif 'NOT IMPLEMENTED:' in err_msg:
LOG.error(_('NOT IMPLEMENTED: The call to %(method)s is not'
' supported by the agent. VM id=%(instance_id)s;'
- ' args=%(strargs)s') % locals())
- raise NotImplementedError(err_msg)
+ ' args=%(args)r') % locals())
+ return {'returncode': 'notimplemented', 'message': err_msg}
else:
LOG.error(_('The call to %(method)s returned an error: %(e)s. '
- 'VM id=%(instance_id)s; args=%(strargs)s') % locals())
+ 'VM id=%(instance_id)s; args=%(args)r') % locals())
+ return {'returncode': 'error', 'message': err_msg}
return ret
def add_to_xenstore(self, vm, path, key, value):
@@ -1313,12 +1334,6 @@ class VMOps(object):
########################################################################
-def _runproc(cmd):
- pipe = subprocess.PIPE
- return subprocess.Popen([cmd], shell=True, stdin=pipe, stdout=pipe,
- stderr=pipe, close_fds=True)
-
-
class SimpleDH(object):
"""
This class wraps all the functionality needed to implement
@@ -1375,22 +1390,18 @@ class SimpleDH(object):
mpi = M2Crypto.m2.bn_to_mpi(bn)
return mpi
- def _run_ssl(self, text, extra_args=None):
- if not extra_args:
- extra_args = ''
- cmd = 'enc -aes-128-cbc -A -a -pass pass:%s -nosalt %s' % (
- self._shared, extra_args)
- proc = _runproc('openssl %s' % cmd)
- proc.stdin.write(text)
- proc.stdin.close()
- proc.wait()
- err = proc.stderr.read()
+ def _run_ssl(self, text, decrypt=False):
+ cmd = ['openssl', 'aes-128-cbc', '-A', '-a', '-pass',
+ 'pass:%s' % self._shared, '-nosalt']
+ if decrypt:
+ cmd.append('-d')
+ out, err = utils.execute(*cmd, process_input=text)
if err:
raise RuntimeError(_('OpenSSL error: %s') % err)
- return proc.stdout.read()
+ return out
def encrypt(self, text):
return self._run_ssl(text).strip('\n')
def decrypt(self, text):
- return self._run_ssl(text, '-d')
+ return self._run_ssl(text, decrypt=True)
diff --git a/nova/virt/xenapi/volume_utils.py b/nova/virt/xenapi/volume_utils.py
index 7821a4f7e..5d5eb824f 100644
--- a/nova/virt/xenapi/volume_utils.py
+++ b/nova/virt/xenapi/volume_utils.py
@@ -252,10 +252,10 @@ def _get_target(volume_id):
volume_id)
result = (None, None)
try:
- (r, _e) = utils.execute('sudo', 'iscsiadm',
+ (r, _e) = utils.execute('iscsiadm',
'-m', 'discovery',
'-t', 'sendtargets',
- '-p', volume_ref['host'])
+ '-p', volume_ref['host'], run_as_root=True)
except exception.ProcessExecutionError, exc:
LOG.exception(exc)
else:
diff --git a/nova/virt/xenapi_conn.py b/nova/virt/xenapi_conn.py
index 39afbd650..76b6c57fc 100644
--- a/nova/virt/xenapi_conn.py
+++ b/nova/virt/xenapi_conn.py
@@ -184,14 +184,14 @@ class XenAPIConnection(driver.ComputeDriver):
def list_instances_detail(self):
return self._vmops.list_instances_detail()
- def spawn(self, context, instance, network_info,
- block_device_mapping=None):
+ def spawn(self, context, instance,
+ network_info=None, block_device_info=None):
"""Create VM instance"""
self._vmops.spawn(context, instance, network_info)
def revert_migration(self, instance):
"""Reverts a resize, powering back on the instance"""
- self._vmops.revert_resize(instance)
+ self._vmops.revert_migration(instance)
def finish_migration(self, context, instance, disk_info, network_info,
resize_instance=False):
@@ -217,7 +217,7 @@ class XenAPIConnection(driver.ComputeDriver):
"""
self._vmops.inject_file(instance, b64_path, b64_contents)
- def destroy(self, instance, network_info):
+ def destroy(self, instance, network_info, cleanup=True):
"""Destroy VM instance"""
self._vmops.destroy(instance, network_info)
@@ -242,13 +242,13 @@ class XenAPIConnection(driver.ComputeDriver):
"""resume the specified instance"""
self._vmops.resume(instance, callback)
- def rescue(self, context, instance, callback, network_info):
+ def rescue(self, context, instance, _callback, network_info):
"""Rescue the specified instance"""
- self._vmops.rescue(context, instance, callback, network_info)
+ self._vmops.rescue(context, instance, _callback, network_info)
- def unrescue(self, instance, callback, network_info):
+ def unrescue(self, instance, _callback, network_info):
"""Unrescue the specified instance"""
- self._vmops.unrescue(instance, callback)
+ self._vmops.unrescue(instance, _callback)
def poll_rescued_instances(self, timeout):
"""Poll for rescued instances"""
@@ -332,6 +332,19 @@ class XenAPIConnection(driver.ComputeDriver):
True, run the update first."""
return self.HostState.get_host_stats(refresh=refresh)
+ def host_power_action(self, host, action):
+ """The only valid values for 'action' on XenServer are 'reboot' or
+ 'shutdown', even though the API also accepts 'startup'. As this is
+ not technically possible on XenServer, since the host is the same
+ physical machine as the hypervisor, if this is requested, we need to
+ raise an exception.
+ """
+ if action in ("reboot", "shutdown"):
+ return self._vmops.host_power_action(host, action)
+ else:
+ msg = _("Host startup on XenServer is not supported.")
+ raise NotImplementedError(msg)
+
def set_host_enabled(self, host, enabled):
"""Sets the specified host's ability to accept new instances."""
return self._vmops.set_host_enabled(host, enabled)
@@ -394,11 +407,10 @@ class XenAPISession(object):
try:
name = self._session.xenapi.task.get_name_label(task)
status = self._session.xenapi.task.get_status(task)
+ # Ensure action is never > 255
+ action = dict(action=name[:255], error=None)
if id:
- action = dict(
- instance_id=int(id),
- action=name[0:255], # Ensure action is never > 255
- error=None)
+ action["instance_id"] = int(id)
if status == "pending":
return
elif status == "success":
@@ -441,7 +453,7 @@ class XenAPISession(object):
params = None
try:
params = eval(exc.details[3])
- except:
+ except Exception:
raise exc
raise self.XenAPI.Failure(params)
else:
diff --git a/nova/vnc/proxy.py b/nova/vnc/proxy.py
index c4603803b..2e3e38ca9 100644
--- a/nova/vnc/proxy.py
+++ b/nova/vnc/proxy.py
@@ -60,7 +60,7 @@ class WebsocketVNCProxy(object):
break
d = base64.b64encode(d)
dest.send(d)
- except:
+ except Exception:
source.close()
dest.close()
@@ -72,7 +72,7 @@ class WebsocketVNCProxy(object):
break
d = base64.b64decode(d)
dest.sendall(d)
- except:
+ except Exception:
source.close()
dest.close()
diff --git a/nova/volume/driver.py b/nova/volume/driver.py
index 23e845deb..c99534c07 100644
--- a/nova/volume/driver.py
+++ b/nova/volume/driver.py
@@ -65,14 +65,14 @@ class VolumeDriver(object):
self._execute = execute
self._sync_exec = sync_exec
- def _try_execute(self, *command):
+ def _try_execute(self, *command, **kwargs):
# NOTE(vish): Volume commands can partially fail due to timing, but
# running them a second time on failure will usually
# recover nicely.
tries = 0
while True:
try:
- self._execute(*command)
+ self._execute(*command, **kwargs)
return True
except exception.ProcessExecutionError:
tries = tries + 1
@@ -84,24 +84,26 @@ class VolumeDriver(object):
def check_for_setup_error(self):
"""Returns an error if prerequisites aren't met"""
- out, err = self._execute('sudo', 'vgs', '--noheadings', '-o', 'name')
+ out, err = self._execute('vgs', '--noheadings', '-o', 'name',
+ run_as_root=True)
volume_groups = out.split()
if not FLAGS.volume_group in volume_groups:
raise exception.Error(_("volume group %s doesn't exist")
% FLAGS.volume_group)
def _create_volume(self, volume_name, sizestr):
- self._try_execute('sudo', 'lvcreate', '-L', sizestr, '-n',
- volume_name, FLAGS.volume_group)
+ self._try_execute('lvcreate', '-L', sizestr, '-n',
+ volume_name, FLAGS.volume_group, run_as_root=True)
def _copy_volume(self, srcstr, deststr, size_in_g):
- self._execute('sudo', 'dd', 'if=%s' % srcstr, 'of=%s' % deststr,
- 'count=%d' % (size_in_g * 1024), 'bs=1M')
+ self._execute('dd', 'if=%s' % srcstr, 'of=%s' % deststr,
+ 'count=%d' % (size_in_g * 1024), 'bs=1M',
+ run_as_root=True)
def _volume_not_present(self, volume_name):
path_name = '%s/%s' % (FLAGS.volume_group, volume_name)
try:
- self._try_execute('sudo', 'lvdisplay', path_name)
+ self._try_execute('lvdisplay', path_name, run_as_root=True)
except Exception as e:
# If the volume isn't present
return True
@@ -112,9 +114,10 @@ class VolumeDriver(object):
# zero out old volumes to prevent data leaking between users
# TODO(ja): reclaiming space should be done lazy and low priority
self._copy_volume('/dev/zero', self.local_path(volume), size_in_g)
- self._try_execute('sudo', 'lvremove', '-f', "%s/%s" %
+ self._try_execute('lvremove', '-f', "%s/%s" %
(FLAGS.volume_group,
- self._escape_snapshot(volume['name'])))
+ self._escape_snapshot(volume['name'])),
+ run_as_root=True)
def _sizestr(self, size_in_g):
if int(size_in_g) == 0:
@@ -147,10 +150,11 @@ class VolumeDriver(object):
# TODO(yamahata): lvm can't delete origin volume only without
# deleting derived snapshots. Can we do something fancy?
- out, err = self._execute('sudo', 'lvdisplay', '--noheading',
+ out, err = self._execute('lvdisplay', '--noheading',
'-C', '-o', 'Attr',
'%s/%s' % (FLAGS.volume_group,
- volume['name']))
+ volume['name']),
+ run_as_root=True)
# fake_execute returns None resulting unit test error
if out:
out = out.strip()
@@ -162,10 +166,10 @@ class VolumeDriver(object):
def create_snapshot(self, snapshot):
"""Creates a snapshot."""
orig_lv_name = "%s/%s" % (FLAGS.volume_group, snapshot['volume_name'])
- self._try_execute('sudo', 'lvcreate', '-L',
+ self._try_execute('lvcreate', '-L',
self._sizestr(snapshot['volume_size']),
'--name', self._escape_snapshot(snapshot['name']),
- '--snapshot', orig_lv_name)
+ '--snapshot', orig_lv_name, run_as_root=True)
def delete_snapshot(self, snapshot):
"""Deletes a snapshot."""
@@ -233,13 +237,14 @@ class AOEDriver(VolumeDriver):
blade_id) = self.db.volume_allocate_shelf_and_blade(context,
volume['id'])
self._try_execute(
- 'sudo', 'vblade-persist', 'setup',
+ 'vblade-persist', 'setup',
shelf_id,
blade_id,
FLAGS.aoe_eth_dev,
"/dev/%s/%s" %
(FLAGS.volume_group,
- volume['name']))
+ volume['name']),
+ run_as_root=True)
# NOTE(vish): The standard _try_execute does not work here
# because these methods throw errors if other
# volumes on this host are in the process of
@@ -248,28 +253,29 @@ class AOEDriver(VolumeDriver):
# just wait a bit for the current volume to
# be ready and ignore any errors.
time.sleep(2)
- self._execute('sudo', 'vblade-persist', 'auto', 'all',
- check_exit_code=False)
- self._execute('sudo', 'vblade-persist', 'start', 'all',
- check_exit_code=False)
+ self._execute('vblade-persist', 'auto', 'all',
+ check_exit_code=False, run_as_root=True)
+ self._execute('vblade-persist', 'start', 'all',
+ check_exit_code=False, run_as_root=True)
def remove_export(self, context, volume):
"""Removes an export for a logical volume."""
(shelf_id,
blade_id) = self.db.volume_get_shelf_and_blade(context,
volume['id'])
- self._try_execute('sudo', 'vblade-persist', 'stop',
- shelf_id, blade_id)
- self._try_execute('sudo', 'vblade-persist', 'destroy',
- shelf_id, blade_id)
+ self._try_execute('vblade-persist', 'stop',
+ shelf_id, blade_id, run_as_root=True)
+ self._try_execute('vblade-persist', 'destroy',
+ shelf_id, blade_id, run_as_root=True)
def discover_volume(self, context, _volume):
"""Discover volume on a remote host."""
(shelf_id,
blade_id) = self.db.volume_get_shelf_and_blade(context,
_volume['id'])
- self._execute('sudo', 'aoe-discover')
- out, err = self._execute('sudo', 'aoe-stat', check_exit_code=False)
+ self._execute('aoe-discover', run_as_root=True)
+ out, err = self._execute('aoe-stat', check_exit_code=False,
+ run_as_root=True)
device_path = 'e%(shelf_id)d.%(blade_id)d' % locals()
if out.find(device_path) >= 0:
return "/dev/etherd/%s" % device_path
@@ -285,8 +291,8 @@ class AOEDriver(VolumeDriver):
(shelf_id,
blade_id) = self.db.volume_get_shelf_and_blade(context,
volume_id)
- cmd = ('sudo', 'vblade-persist', 'ls', '--no-header')
- out, _err = self._execute(*cmd)
+ cmd = ('vblade-persist', 'ls', '--no-header')
+ out, _err = self._execute(*cmd, run_as_root=True)
exported = False
for line in out.split('\n'):
param = line.split(' ')
@@ -348,16 +354,18 @@ class ISCSIDriver(VolumeDriver):
iscsi_name = "%s%s" % (FLAGS.iscsi_target_prefix, volume['name'])
volume_path = "/dev/%s/%s" % (FLAGS.volume_group, volume['name'])
- self._sync_exec('sudo', 'ietadm', '--op', 'new',
+ self._sync_exec('ietadm', '--op', 'new',
"--tid=%s" % iscsi_target,
'--params',
"Name=%s" % iscsi_name,
+ run_as_root=True,
check_exit_code=False)
- self._sync_exec('sudo', 'ietadm', '--op', 'new',
+ self._sync_exec('ietadm', '--op', 'new',
"--tid=%s" % iscsi_target,
'--lun=0',
'--params',
"Path=%s,Type=fileio" % volume_path,
+ run_as_root=True,
check_exit_code=False)
def _ensure_iscsi_targets(self, context, host):
@@ -378,13 +386,13 @@ class ISCSIDriver(VolumeDriver):
volume['host'])
iscsi_name = "%s%s" % (FLAGS.iscsi_target_prefix, volume['name'])
volume_path = "/dev/%s/%s" % (FLAGS.volume_group, volume['name'])
- self._execute('sudo', 'ietadm', '--op', 'new',
+ self._execute('ietadm', '--op', 'new',
'--tid=%s' % iscsi_target,
- '--params', 'Name=%s' % iscsi_name)
- self._execute('sudo', 'ietadm', '--op', 'new',
+ '--params', 'Name=%s' % iscsi_name, run_as_root=True)
+ self._execute('ietadm', '--op', 'new',
'--tid=%s' % iscsi_target,
'--lun=0', '--params',
- 'Path=%s,Type=fileio' % volume_path)
+ 'Path=%s,Type=fileio' % volume_path, run_as_root=True)
def remove_export(self, context, volume):
"""Removes an export for a logical volume."""
@@ -399,18 +407,18 @@ class ISCSIDriver(VolumeDriver):
try:
# ietadm show will exit with an error
# this export has already been removed
- self._execute('sudo', 'ietadm', '--op', 'show',
- '--tid=%s' % iscsi_target)
+ self._execute('ietadm', '--op', 'show',
+ '--tid=%s' % iscsi_target, run_as_root=True)
except Exception as e:
LOG.info(_("Skipping remove_export. No iscsi_target " +
"is presently exported for volume: %d"), volume['id'])
return
- self._execute('sudo', 'ietadm', '--op', 'delete',
+ self._execute('ietadm', '--op', 'delete',
'--tid=%s' % iscsi_target,
- '--lun=0')
- self._execute('sudo', 'ietadm', '--op', 'delete',
- '--tid=%s' % iscsi_target)
+ '--lun=0', run_as_root=True)
+ self._execute('ietadm', '--op', 'delete',
+ '--tid=%s' % iscsi_target, run_as_root=True)
def _do_iscsi_discovery(self, volume):
#TODO(justinsb): Deprecate discovery and use stored info
@@ -419,8 +427,9 @@ class ISCSIDriver(VolumeDriver):
volume_name = volume['name']
- (out, _err) = self._execute('sudo', 'iscsiadm', '-m', 'discovery',
- '-t', 'sendtargets', '-p', volume['host'])
+ (out, _err) = self._execute('iscsiadm', '-m', 'discovery',
+ '-t', 'sendtargets', '-p', volume['host'],
+ run_as_root=True)
for target in out.splitlines():
if FLAGS.iscsi_ip_prefix in target and volume_name in target:
return target
@@ -483,10 +492,10 @@ class ISCSIDriver(VolumeDriver):
return properties
def _run_iscsiadm(self, iscsi_properties, iscsi_command):
- (out, err) = self._execute('sudo', 'iscsiadm', '-m', 'node', '-T',
+ (out, err) = self._execute('iscsiadm', '-m', 'node', '-T',
iscsi_properties['target_iqn'],
'-p', iscsi_properties['target_portal'],
- iscsi_command)
+ iscsi_command, run_as_root=True)
LOG.debug("iscsiadm %s: stdout=%s stderr=%s" %
(iscsi_command, out, err))
return (out, err)
@@ -560,8 +569,8 @@ class ISCSIDriver(VolumeDriver):
tid = self.db.volume_get_iscsi_target_num(context, volume_id)
try:
- self._execute('sudo', 'ietadm', '--op', 'show',
- '--tid=%(tid)d' % locals())
+ self._execute('ietadm', '--op', 'show',
+ '--tid=%(tid)d' % locals(), run_as_root=True)
except exception.ProcessExecutionError, e:
# Instances remount read-only in this case.
# /etc/init.d/iscsitarget restart and rebooting nova-volume