summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorVishvananda Ishaya <vishvananda@gmail.com>2011-07-18 16:58:23 -0700
committerVishvananda Ishaya <vishvananda@gmail.com>2011-07-18 16:58:23 -0700
commitb5ceab5a46ffac11cb229de86c49802bba3fa383 (patch)
tree62459fbead53019efe4efc3e762fe1e07e84a450
parent67e5492d6723a00b0ad5d7e8c44f5762a9b0a206 (diff)
parent77db06c908f9c08c80beb11241c0e23247129ad6 (diff)
downloadnova-b5ceab5a46ffac11cb229de86c49802bba3fa383.tar.gz
nova-b5ceab5a46ffac11cb229de86c49802bba3fa383.tar.xz
nova-b5ceab5a46ffac11cb229de86c49802bba3fa383.zip
merged trunk
-rw-r--r--Authors6
-rwxr-xr-xbin/nova-manage57
-rwxr-xr-xcontrib/nova.sh20
-rw-r--r--doc/source/devref/index.rst7
-rw-r--r--doc/source/devref/multinic.rst4
-rw-r--r--nova/api/ec2/__init__.py7
-rw-r--r--nova/api/ec2/cloud.py340
-rw-r--r--nova/api/ec2/ec2utils.py40
-rw-r--r--nova/api/ec2/metadatarequesthandler.py6
-rw-r--r--nova/api/openstack/__init__.py12
-rw-r--r--nova/api/openstack/accounts.py6
-rw-r--r--nova/api/openstack/backup_schedules.py13
-rw-r--r--nova/api/openstack/common.py26
-rw-r--r--nova/api/openstack/consoles.py12
-rw-r--r--nova/api/openstack/contrib/floating_ips.py4
-rw-r--r--nova/api/openstack/contrib/multinic.py125
-rw-r--r--nova/api/openstack/create_instance_helper.py2
-rw-r--r--nova/api/openstack/flavors.py6
-rw-r--r--nova/api/openstack/image_metadata.py5
-rw-r--r--nova/api/openstack/images.py137
-rw-r--r--nova/api/openstack/ips.py82
-rw-r--r--nova/api/openstack/limits.py114
-rw-r--r--nova/api/openstack/server_metadata.py6
-rw-r--r--nova/api/openstack/servers.py71
-rw-r--r--nova/api/openstack/shared_ip_groups.py12
-rw-r--r--nova/api/openstack/users.py6
-rw-r--r--nova/api/openstack/versions.py5
-rw-r--r--nova/api/openstack/views/addresses.py39
-rw-r--r--nova/api/openstack/views/images.py36
-rw-r--r--nova/api/openstack/views/servers.py13
-rw-r--r--nova/api/openstack/wsgi.py218
-rw-r--r--nova/api/openstack/zones.py9
-rw-r--r--nova/compute/api.py102
-rw-r--r--nova/compute/manager.py148
-rw-r--r--nova/console/manager.py4
-rw-r--r--nova/console/vmrc_manager.py4
-rw-r--r--nova/db/api.py8
-rw-r--r--nova/db/sqlalchemy/api.py41
-rw-r--r--nova/db/sqlalchemy/migrate_repo/versions/032_add_root_device_name.py47
-rw-r--r--nova/db/sqlalchemy/models.py2
-rw-r--r--nova/exception.py63
-rw-r--r--nova/image/fake.py11
-rw-r--r--nova/image/s3.py53
-rw-r--r--nova/network/api.py8
-rw-r--r--nova/network/manager.py49
-rw-r--r--nova/notifier/api.py14
-rw-r--r--nova/rpc.py2
-rw-r--r--nova/scheduler/driver.py1
-rw-r--r--nova/scheduler/zone_aware_scheduler.py8
-rw-r--r--nova/scheduler/zone_manager.py32
-rw-r--r--nova/test.py16
-rw-r--r--nova/tests/api/openstack/contrib/test_floating_ips.py3
-rw-r--r--nova/tests/api/openstack/contrib/test_multinic_xs.py115
-rw-r--r--nova/tests/api/openstack/test_common.py57
-rw-r--r--nova/tests/api/openstack/test_images.py557
-rw-r--r--nova/tests/api/openstack/test_limits.py96
-rw-r--r--nova/tests/api/openstack/test_servers.py292
-rw-r--r--nova/tests/api/openstack/test_wsgi.py141
-rw-r--r--nova/tests/image/test_s3.py122
-rw-r--r--nova/tests/integrated/api/client.py23
-rw-r--r--nova/tests/integrated/test_servers.py19
-rw-r--r--nova/tests/scheduler/test_zone_aware_scheduler.py6
-rw-r--r--nova/tests/test_api.py70
-rw-r--r--nova/tests/test_bdm.py233
-rw-r--r--nova/tests/test_cloud.py447
-rw-r--r--nova/tests/test_compute.py111
-rw-r--r--nova/tests/test_exception.py63
-rw-r--r--nova/tests/test_metadata.py76
-rw-r--r--nova/tests/test_network.py33
-rw-r--r--nova/tests/test_volume.py31
-rw-r--r--nova/tests/test_zones.py175
-rw-r--r--nova/virt/driver.py4
-rw-r--r--nova/virt/libvirt/connection.py30
-rw-r--r--nova/virt/xenapi/vmops.py12
-rw-r--r--nova/volume/api.py13
-rw-r--r--plugins/xenserver/xenapi/contrib/rpmbuild/SPECS/openstack-xen-plugins.spec1
-rwxr-xr-xplugins/xenserver/xenapi/etc/xapi.d/plugins/agent51
-rwxr-xr-xtools/clean-vlans6
-rwxr-xr-xtools/nova-debug21
79 files changed, 4088 insertions, 779 deletions
diff --git a/Authors b/Authors
index 1b3c90764..8ffb7d8d4 100644
--- a/Authors
+++ b/Authors
@@ -1,4 +1,5 @@
Alex Meade <alex.meade@rackspace.com>
+Alexander Sakhnov <asakhnov@mirantis.com>
Andrey Brindeyev <abrindeyev@griddynamics.com>
Andy Smith <code@term.ie>
Andy Southgate <andy.southgate@citrix.com>
@@ -20,6 +21,7 @@ Dan Prince <dan.prince@rackspace.com>
Dave Walker <DaveWalker@ubuntu.com>
David Pravec <David.Pravec@danix.org>
Dean Troyer <dtroyer@gmail.com>
+Devendra Modium <dmodium@isi.edu>
Devin Carlen <devin.carlen@gmail.com>
Ed Leafe <ed@leafe.com>
Eldar Nugaev <reldan@oscloud.ru>
@@ -43,6 +45,7 @@ John Dewey <john@dewey.ws>
John Tran <jtran@attinteractive.com>
Jonathan Bryce <jbryce@jbryce.com>
Jordan Rinke <jordan@openstack.org>
+Joseph Suh <jsuh@isi.edu>
Josh Durgin <joshd@hq.newdream.net>
Josh Kearney <josh@jk0.org>
Josh Kleinpeter <josh@kleinpeter.org>
@@ -62,6 +65,7 @@ Masanori Itoh <itoumsn@nttdata.co.jp>
Matt Dietz <matt.dietz@rackspace.com>
Michael Gundlach <michael.gundlach@rackspace.com>
Mike Scherbakov <mihgen@gmail.com>
+Mohammed Naser <mnaser@vexxhost.com>
Monsyne Dragon <mdragon@rackspace.com>
Monty Taylor <mordred@inaugust.com>
MORITA Kazutaka <morita.kazutaka@gmail.com>
@@ -81,7 +85,9 @@ Ryan Lucio <rlucio@internap.com>
Salvatore Orlando <salvatore.orlando@eu.citrix.com>
Sandy Walsh <sandy.walsh@rackspace.com>
Sateesh Chodapuneedi <sateesh.chodapuneedi@citrix.com>
+Scott Moser <smoser@ubuntu.com>
Soren Hansen <soren.hansen@rackspace.com>
+Stephanie Reese <reese.sm@gmail.com>
Thierry Carrez <thierry@openstack.org>
Todd Willey <todd@ansolabs.com>
Trey Morris <trey.morris@rackspace.com>
diff --git a/bin/nova-manage b/bin/nova-manage
index a37d68f42..8f7319f7a 100755
--- a/bin/nova-manage
+++ b/bin/nova-manage
@@ -414,8 +414,11 @@ class ProjectCommands(object):
except (exception.UserNotFound, exception.ProjectNotFound) as ex:
print ex
raise
- with open(filename, 'w') as f:
- f.write(rc)
+ if filename == "-":
+ sys.stdout.write(rc)
+ else:
+ with open(filename, 'w') as f:
+ f.write(rc)
def list(self, username=None):
"""Lists all projects
@@ -465,8 +468,11 @@ class ProjectCommands(object):
arguments: project_id user_id [filename='nova.zip]"""
try:
zip_file = self.manager.get_credentials(user_id, project_id)
- with open(filename, 'w') as f:
- f.write(zip_file)
+ if filename == "-":
+ sys.stdout.write(zip_file)
+ else:
+ with open(filename, 'w') as f:
+ f.write(zip_file)
except (exception.UserNotFound, exception.ProjectNotFound) as ex:
print ex
raise
@@ -620,15 +626,19 @@ class NetworkCommands(object):
def list(self):
"""List all created networks"""
- print "%-18s\t%-15s\t%-15s\t%-15s" % (_('network'),
- _('netmask'),
- _('start address'),
- 'DNS')
+ print "%-18s\t%-15s\t%-15s\t%-15s\t%-15s\t%-15s" % (_('network'),
+ _('netmask'),
+ _('start address'),
+ _('DNS'),
+ _('VlanID'),
+ 'project')
for network in db.network_get_all(context.get_admin_context()):
- print "%-18s\t%-15s\t%-15s\t%-15s" % (network.cidr,
- network.netmask,
- network.dhcp_start,
- network.dns)
+ print "%-18s\t%-15s\t%-15s\t%-15s\t%-15s\t%-15s" % (network.cidr,
+ network.netmask,
+ network.dhcp_start,
+ network.dns,
+ network.vlan,
+ network.project_id)
def delete(self, fixed_range):
"""Deletes a network"""
@@ -817,6 +827,28 @@ class ServiceCommands(object):
{"method": "update_available_resource"})
+class HostCommands(object):
+ """List hosts"""
+
+ def list(self, zone=None):
+ """Show a list of all physical hosts. Filter by zone.
+ args: [zone]"""
+ print "%-25s\t%-15s" % (_('host'),
+ _('zone'))
+ ctxt = context.get_admin_context()
+ now = utils.utcnow()
+ services = db.service_get_all(ctxt)
+ if zone:
+ services = [s for s in services if s['availability_zone'] == zone]
+ hosts = []
+ for srv in services:
+ if not [h for h in hosts if h['host'] == srv['host']]:
+ hosts.append(srv)
+
+ for h in hosts:
+ print "%-25s\t%-15s" % (h['host'], h['availability_zone'])
+
+
class DbCommands(object):
"""Class for managing the database."""
@@ -1190,6 +1222,7 @@ CATEGORIES = [
('fixed', FixedIpCommands),
('flavor', InstanceTypeCommands),
('floating', FloatingIpCommands),
+ ('host', HostCommands),
('instance_type', InstanceTypeCommands),
('image', ImageCommands),
('network', NetworkCommands),
diff --git a/contrib/nova.sh b/contrib/nova.sh
index d7d34dcbd..eab680580 100755
--- a/contrib/nova.sh
+++ b/contrib/nova.sh
@@ -17,7 +17,7 @@ if [ ! -n "$HOST_IP" ]; then
HOST_IP=`LC_ALL=C ifconfig | grep -m 1 'inet addr:'| cut -d: -f2 | awk '{print $1}'`
fi
-USE_MYSQL=${USE_MYSQL:-0}
+USE_MYSQL=${USE_MYSQL:-1}
INTERFACE=${INTERFACE:-eth0}
FLOATING_RANGE=${FLOATING_RANGE:-10.6.0.0/27}
FIXED_RANGE=${FIXED_RANGE:-10.0.0.0/24}
@@ -159,10 +159,6 @@ NOVA_CONF_EOF
mkdir -p $NOVA_DIR/instances
rm -rf $NOVA_DIR/networks
mkdir -p $NOVA_DIR/networks
- if [ ! -d "$NOVA_DIR/images" ]; then
- ln -s $DIR/images $NOVA_DIR/images
- fi
-
if [ "$TEST" == 1 ]; then
cd $NOVA_DIR
python $NOVA_DIR/run_tests.py
@@ -181,8 +177,18 @@ NOVA_CONF_EOF
# create some floating ips
$NOVA_DIR/bin/nova-manage floating create `hostname` $FLOATING_RANGE
- # convert old images
- $NOVA_DIR/bin/nova-manage image convert $DIR/images
+ if [ ! -d "$NOVA_DIR/images" ]; then
+ if [ ! -d "$DIR/converted-images" ]; then
+ # convert old images
+ mkdir $DIR/converted-images
+ ln -s $DIR/converted-images $NOVA_DIR/images
+ $NOVA_DIR/bin/nova-manage image convert $DIR/images
+ else
+ ln -s $DIR/converted-images $NOVA_DIR/images
+ fi
+
+ fi
+
# nova api crashes if we start it with a regular screen command,
# so send the start command by forcing text into the window.
diff --git a/doc/source/devref/index.rst b/doc/source/devref/index.rst
index 0a5a7a4d6..859d4e331 100644
--- a/doc/source/devref/index.rst
+++ b/doc/source/devref/index.rst
@@ -30,13 +30,16 @@ Programming HowTos and Tutorials
addmethod.openstackapi
-Programming Concepts
---------------------
+Background Concepts for Nova
+----------------------------
.. toctree::
:maxdepth: 3
+ distributed_scheduler
+ multinic
zone
rabbit
+
API Reference
-------------
diff --git a/doc/source/devref/multinic.rst b/doc/source/devref/multinic.rst
index b3a82d341..43830258f 100644
--- a/doc/source/devref/multinic.rst
+++ b/doc/source/devref/multinic.rst
@@ -29,11 +29,11 @@ FlatDHCP Manager
.. image:: /images/multinic_dhcp.png
-FlatDHCP manager builds on the the Flat manager adding dnsmask (DNS and DHCP) and radvd (Router Advertisement) servers on the bridge for that network. The services run on the host that is assigned to that nework. The FlatDHCP manager will create its bridge as specified when the network was created on the network-host when the network host starts up or when a new network gets allocated to that host. Compute nodes will also create the bridges as necessary and connect instance VIFs to them.
+FlatDHCP manager builds on the the Flat manager adding dnsmask (DNS and DHCP) and radvd (Router Advertisement) servers on the bridge for that network. The services run on the host that is assigned to that network. The FlatDHCP manager will create its bridge as specified when the network was created on the network-host when the network host starts up or when a new network gets allocated to that host. Compute nodes will also create the bridges as necessary and connect instance VIFs to them.
VLAN Manager
------------
.. image:: /images/multinic_vlan.png
-The VLAN manager sets up forwarding to/from a cloudpipe instance in addition to providing dnsmask (DNS and DHCP) and radvd (Router Advertisement) services for each network. The manager will create its bridge as specified when the network was created on the network-host when the network host starts up or when a new network gets allocated to that host. Compute nodes will also create the bridges as necessary and conenct instance VIFs to them.
+The VLAN manager sets up forwarding to/from a cloudpipe instance in addition to providing dnsmask (DNS and DHCP) and radvd (Router Advertisement) services for each network. The manager will create its bridge as specified when the network was created on the network-host when the network host starts up or when a new network gets allocated to that host. Compute nodes will also create the bridges as necessary and connect instance VIFs to them.
diff --git a/nova/api/ec2/__init__.py b/nova/api/ec2/__init__.py
index 890d57fe7..cf1734281 100644
--- a/nova/api/ec2/__init__.py
+++ b/nova/api/ec2/__init__.py
@@ -262,6 +262,8 @@ class Authorizer(wsgi.Middleware):
'TerminateInstances': ['projectmanager', 'sysadmin'],
'RebootInstances': ['projectmanager', 'sysadmin'],
'UpdateInstance': ['projectmanager', 'sysadmin'],
+ 'StartInstances': ['projectmanager', 'sysadmin'],
+ 'StopInstances': ['projectmanager', 'sysadmin'],
'DeleteVolume': ['projectmanager', 'sysadmin'],
'DescribeImages': ['all'],
'DeregisterImage': ['projectmanager', 'sysadmin'],
@@ -269,6 +271,7 @@ class Authorizer(wsgi.Middleware):
'DescribeImageAttribute': ['all'],
'ModifyImageAttribute': ['projectmanager', 'sysadmin'],
'UpdateImage': ['projectmanager', 'sysadmin'],
+ 'CreateImage': ['projectmanager', 'sysadmin'],
},
'AdminController': {
# All actions have the same permission: ['none'] (the default)
@@ -325,13 +328,13 @@ class Executor(wsgi.Application):
except exception.VolumeNotFound as ex:
LOG.info(_('VolumeNotFound raised: %s'), unicode(ex),
context=context)
- ec2_id = ec2utils.id_to_ec2_id(ex.volume_id, 'vol-%08x')
+ ec2_id = ec2utils.id_to_ec2_vol_id(ex.volume_id)
message = _('Volume %s not found') % ec2_id
return self._error(req, context, type(ex).__name__, message)
except exception.SnapshotNotFound as ex:
LOG.info(_('SnapshotNotFound raised: %s'), unicode(ex),
context=context)
- ec2_id = ec2utils.id_to_ec2_id(ex.snapshot_id, 'snap-%08x')
+ ec2_id = ec2utils.id_to_ec2_snap_id(ex.snapshot_id)
message = _('Snapshot %s not found') % ec2_id
return self._error(req, context, type(ex).__name__, message)
except exception.NotFound as ex:
diff --git a/nova/api/ec2/cloud.py b/nova/api/ec2/cloud.py
index 9be30cf75..16ca1ed2a 100644
--- a/nova/api/ec2/cloud.py
+++ b/nova/api/ec2/cloud.py
@@ -27,6 +27,7 @@ import netaddr
import os
import urllib
import tempfile
+import time
import shutil
from nova import compute
@@ -75,6 +76,95 @@ def _gen_key(context, user_id, key_name):
return {'private_key': private_key, 'fingerprint': fingerprint}
+# TODO(yamahata): hypervisor dependent default device name
+_DEFAULT_ROOT_DEVICE_NAME = '/dev/sda1'
+
+
+def _parse_block_device_mapping(bdm):
+ """Parse BlockDeviceMappingItemType into flat hash
+ BlockDevicedMapping.<N>.DeviceName
+ BlockDevicedMapping.<N>.Ebs.SnapshotId
+ BlockDevicedMapping.<N>.Ebs.VolumeSize
+ BlockDevicedMapping.<N>.Ebs.DeleteOnTermination
+ BlockDevicedMapping.<N>.Ebs.NoDevice
+ BlockDevicedMapping.<N>.VirtualName
+ => remove .Ebs and allow volume id in SnapshotId
+ """
+ ebs = bdm.pop('ebs', None)
+ if ebs:
+ ec2_id = ebs.pop('snapshot_id', None)
+ if ec2_id:
+ id = ec2utils.ec2_id_to_id(ec2_id)
+ if ec2_id.startswith('snap-'):
+ bdm['snapshot_id'] = id
+ elif ec2_id.startswith('vol-'):
+ bdm['volume_id'] = id
+ ebs.setdefault('delete_on_termination', True)
+ bdm.update(ebs)
+ return bdm
+
+
+def _properties_get_mappings(properties):
+ return ec2utils.mappings_prepend_dev(properties.get('mappings', []))
+
+
+def _format_block_device_mapping(bdm):
+ """Contruct BlockDeviceMappingItemType
+ {'device_name': '...', 'snapshot_id': , ...}
+ => BlockDeviceMappingItemType
+ """
+ keys = (('deviceName', 'device_name'),
+ ('virtualName', 'virtual_name'))
+ item = {}
+ for name, k in keys:
+ if k in bdm:
+ item[name] = bdm[k]
+ if bdm.get('no_device'):
+ item['noDevice'] = True
+ if ('snapshot_id' in bdm) or ('volume_id' in bdm):
+ ebs_keys = (('snapshotId', 'snapshot_id'),
+ ('snapshotId', 'volume_id'), # snapshotId is abused
+ ('volumeSize', 'volume_size'),
+ ('deleteOnTermination', 'delete_on_termination'))
+ ebs = {}
+ for name, k in ebs_keys:
+ if k in bdm:
+ if k == 'snapshot_id':
+ ebs[name] = ec2utils.id_to_ec2_snap_id(bdm[k])
+ elif k == 'volume_id':
+ ebs[name] = ec2utils.id_to_ec2_vol_id(bdm[k])
+ else:
+ ebs[name] = bdm[k]
+ assert 'snapshotId' in ebs
+ item['ebs'] = ebs
+ return item
+
+
+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'))]
+
+ block_device_mapping = [_format_block_device_mapping(bdm) for bdm in
+ properties.get('block_device_mapping', [])]
+
+ # NOTE(yamahata): overwrite mappings with block_device_mapping
+ for bdm in block_device_mapping:
+ for i in range(len(mappings)):
+ if bdm['deviceName'] == mappings[i]['deviceName']:
+ del mappings[i]
+ break
+ mappings.append(bdm)
+
+ # NOTE(yamahata): trim ebs.no_device == true. Is this necessary?
+ mappings = [bdm for bdm in mappings if not (bdm.get('noDevice', False))]
+
+ if mappings:
+ result['blockDeviceMapping'] = mappings
+
+
class CloudController(object):
""" CloudController provides the critical dispatch between
inbound API calls through the endpoint and messages
@@ -166,6 +256,9 @@ class CloudController(object):
instance_ref['id'])
ec2_id = ec2utils.id_to_ec2_id(instance_ref['id'])
image_ec2_id = self.image_ec2_id(instance_ref['image_ref'])
+ security_groups = db.security_group_get_by_instance(ctxt,
+ instance_ref['id'])
+ security_groups = [x['name'] for x in security_groups]
data = {
'user-data': base64.b64decode(instance_ref['user_data']),
'meta-data': {
@@ -176,7 +269,7 @@ class CloudController(object):
# TODO(vish): replace with real data
'ami': 'sda1',
'ephemeral0': 'sda2',
- 'root': '/dev/sda1',
+ 'root': _DEFAULT_ROOT_DEVICE_NAME,
'swap': 'sda3'},
'hostname': hostname,
'instance-action': 'none',
@@ -189,7 +282,7 @@ class CloudController(object):
'public-ipv4': floating_ip or '',
'public-keys': keys,
'reservation-id': instance_ref['reservation_id'],
- 'security-groups': '',
+ 'security-groups': security_groups,
'mpi': mpi}}
for image_type in ['kernel', 'ramdisk']:
@@ -304,9 +397,8 @@ class CloudController(object):
def _format_snapshot(self, context, snapshot):
s = {}
- s['snapshotId'] = ec2utils.id_to_ec2_id(snapshot['id'], 'snap-%08x')
- s['volumeId'] = ec2utils.id_to_ec2_id(snapshot['volume_id'],
- 'vol-%08x')
+ s['snapshotId'] = ec2utils.id_to_ec2_snap_id(snapshot['id'])
+ s['volumeId'] = ec2utils.id_to_ec2_vol_id(snapshot['volume_id'])
s['status'] = snapshot['status']
s['startTime'] = snapshot['created_at']
s['progress'] = snapshot['progress']
@@ -683,7 +775,7 @@ class CloudController(object):
instance_data = '%s[%s]' % (instance_ec2_id,
volume['instance']['host'])
v = {}
- v['volumeId'] = ec2utils.id_to_ec2_id(volume['id'], 'vol-%08x')
+ v['volumeId'] = ec2utils.id_to_ec2_vol_id(volume['id'])
v['status'] = volume['status']
v['size'] = volume['size']
v['availabilityZone'] = volume['availability_zone']
@@ -705,8 +797,7 @@ class CloudController(object):
else:
v['attachmentSet'] = [{}]
if volume.get('snapshot_id') != None:
- v['snapshotId'] = ec2utils.id_to_ec2_id(volume['snapshot_id'],
- 'snap-%08x')
+ v['snapshotId'] = ec2utils.id_to_ec2_snap_id(volume['snapshot_id'])
else:
v['snapshotId'] = None
@@ -769,7 +860,7 @@ class CloudController(object):
'instanceId': ec2utils.id_to_ec2_id(instance_id),
'requestId': context.request_id,
'status': volume['attach_status'],
- 'volumeId': ec2utils.id_to_ec2_id(volume_id, 'vol-%08x')}
+ 'volumeId': ec2utils.id_to_ec2_vol_id(volume_id)}
def detach_volume(self, context, volume_id, **kwargs):
volume_id = ec2utils.ec2_id_to_id(volume_id)
@@ -781,7 +872,7 @@ class CloudController(object):
'instanceId': ec2utils.id_to_ec2_id(instance['id']),
'requestId': context.request_id,
'status': volume['attach_status'],
- 'volumeId': ec2utils.id_to_ec2_id(volume_id, 'vol-%08x')}
+ 'volumeId': ec2utils.id_to_ec2_vol_id(volume_id)}
def _convert_to_set(self, lst, label):
if lst is None or lst == []:
@@ -805,6 +896,37 @@ class CloudController(object):
assert len(i) == 1
return i[0]
+ def _format_instance_bdm(self, context, instance_id, root_device_name,
+ result):
+ """Format InstanceBlockDeviceMappingResponseItemType"""
+ root_device_type = 'instance-store'
+ mapping = []
+ for bdm in db.block_device_mapping_get_all_by_instance(context,
+ instance_id):
+ volume_id = bdm['volume_id']
+ if (volume_id is None or bdm['no_device']):
+ continue
+
+ if (bdm['device_name'] == root_device_name and
+ (bdm['snapshot_id'] or bdm['volume_id'])):
+ assert not bdm['virtual_name']
+ root_device_type = 'ebs'
+
+ vol = self.volume_api.get(context, volume_id=volume_id)
+ LOG.debug(_("vol = %s\n"), vol)
+ # TODO(yamahata): volume attach time
+ ebs = {'volumeId': volume_id,
+ 'deleteOnTermination': bdm['delete_on_termination'],
+ 'attachTime': vol['attach_time'] or '-',
+ 'status': vol['status'], }
+ res = {'deviceName': bdm['device_name'],
+ 'ebs': ebs, }
+ mapping.append(res)
+
+ if mapping:
+ result['blockDeviceMapping'] = mapping
+ result['rootDeviceType'] = root_device_type
+
def _format_instances(self, context, instance_id=None, **kwargs):
# TODO(termie): this method is poorly named as its name does not imply
# that it will be making a variety of database calls
@@ -866,6 +988,10 @@ class CloudController(object):
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_bdm(context, instance_id,
+ i['rootDeviceName'], i)
host = instance['host']
zone = self._get_availability_zone_by_host(context, host)
i['placement'] = {'availabilityZone': zone}
@@ -953,23 +1079,7 @@ class CloudController(object):
ramdisk = self._get_image(context, kwargs['ramdisk_id'])
kwargs['ramdisk_id'] = ramdisk['id']
for bdm in kwargs.get('block_device_mapping', []):
- # NOTE(yamahata)
- # BlockDevicedMapping.<N>.DeviceName
- # BlockDevicedMapping.<N>.Ebs.SnapshotId
- # BlockDevicedMapping.<N>.Ebs.VolumeSize
- # BlockDevicedMapping.<N>.Ebs.DeleteOnTermination
- # BlockDevicedMapping.<N>.VirtualName
- # => remove .Ebs and allow volume id in SnapshotId
- ebs = bdm.pop('ebs', None)
- if ebs:
- ec2_id = ebs.pop('snapshot_id')
- id = ec2utils.ec2_id_to_id(ec2_id)
- if ec2_id.startswith('snap-'):
- bdm['snapshot_id'] = id
- elif ec2_id.startswith('vol-'):
- bdm['volume_id'] = id
- ebs.setdefault('delete_on_termination', True)
- bdm.update(ebs)
+ _parse_block_device_mapping(bdm)
image = self._get_image(context, kwargs['image_id'])
@@ -1088,12 +1198,16 @@ class CloudController(object):
def _get_image(self, context, ec2_id):
try:
internal_id = ec2utils.ec2_id_to_id(ec2_id)
- return self.image_service.show(context, internal_id)
+ image = self.image_service.show(context, internal_id)
except (exception.InvalidEc2Id, exception.ImageNotFound):
try:
return self.image_service.show_by_name(context, ec2_id)
except exception.NotFound:
raise exception.ImageNotFound(image_id=ec2_id)
+ image_type = ec2_id.split('-')[0]
+ if self._image_type(image.get('container_format')) != image_type:
+ raise exception.ImageNotFound(image_id=ec2_id)
+ return image
def _format_image(self, image):
"""Convert from format defined by BaseImageService to S3 format."""
@@ -1124,6 +1238,20 @@ class CloudController(object):
i['imageType'] = display_mapping.get(image_type)
i['isPublic'] = image.get('is_public') == True
i['architecture'] = image['properties'].get('architecture')
+
+ properties = image['properties']
+ root_device_name = ec2utils.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
+ ('snapshot_id' in bdm or 'volume_id' in bdm) and
+ not bdm.get('no_device')):
+ root_device_type = 'ebs'
+ i['rootDeviceName'] = (root_device_name or _DEFAULT_ROOT_DEVICE_NAME)
+ i['rootDeviceType'] = root_device_type
+
+ _format_mappings(properties, i)
+
return i
def describe_images(self, context, image_id=None, **kwargs):
@@ -1148,30 +1276,64 @@ class CloudController(object):
self.image_service.delete(context, internal_id)
return {'imageId': image_id}
+ def _register_image(self, context, metadata):
+ image = self.image_service.create(context, metadata)
+ image_type = self._image_type(image.get('container_format'))
+ image_id = self.image_ec2_id(image['id'], image_type)
+ return image_id
+
def register_image(self, context, image_location=None, **kwargs):
if image_location is None and 'name' in kwargs:
image_location = kwargs['name']
metadata = {'properties': {'image_location': image_location}}
- image = self.image_service.create(context, metadata)
- image_type = self._image_type(image.get('container_format'))
- image_id = self.image_ec2_id(image['id'],
- image_type)
+
+ if 'root_device_name' in kwargs:
+ metadata['properties']['root_device_name'] = \
+ kwargs.get('root_device_name')
+
+ mappings = [_parse_block_device_mapping(bdm) for bdm in
+ kwargs.get('block_device_mapping', [])]
+ if mappings:
+ metadata['properties']['block_device_mapping'] = mappings
+
+ image_id = self._register_image(context, metadata)
msg = _("Registered image %(image_location)s with"
" id %(image_id)s") % locals()
LOG.audit(msg, context=context)
return {'imageId': image_id}
def describe_image_attribute(self, context, image_id, attribute, **kwargs):
- if attribute != 'launchPermission':
+ def _block_device_mapping_attribute(image, result):
+ _format_mappings(image['properties'], result)
+
+ def _launch_permission_attribute(image, result):
+ result['launchPermission'] = []
+ if image['is_public']:
+ result['launchPermission'].append({'group': 'all'})
+
+ def _root_device_name_attribute(image, result):
+ result['rootDeviceName'] = \
+ ec2utils.properties_root_device_name(image['properties'])
+ if result['rootDeviceName'] is None:
+ result['rootDeviceName'] = _DEFAULT_ROOT_DEVICE_NAME
+
+ supported_attributes = {
+ 'blockDeviceMapping': _block_device_mapping_attribute,
+ 'launchPermission': _launch_permission_attribute,
+ 'rootDeviceName': _root_device_name_attribute,
+ }
+
+ fn = supported_attributes.get(attribute)
+ if fn is None:
raise exception.ApiError(_('attribute not supported: %s')
% attribute)
try:
image = self._get_image(context, image_id)
except exception.NotFound:
raise exception.ImageNotFound(image_id=image_id)
- result = {'imageId': image_id, 'launchPermission': []}
- if image['is_public']:
- result['launchPermission'].append({'group': 'all'})
+
+ result = {'imageId': image_id}
+ fn(image, result)
return result
def modify_image_attribute(self, context, image_id, attribute,
@@ -1202,3 +1364,109 @@ class CloudController(object):
internal_id = ec2utils.ec2_id_to_id(image_id)
result = self.image_service.update(context, internal_id, dict(kwargs))
return result
+
+ # TODO(yamahata): race condition
+ # At the moment there is no way to prevent others from
+ # manipulating instances/volumes/snapshots.
+ # As other code doesn't take it into consideration, here we don't
+ # care of it for now. Ostrich algorithm
+ def create_image(self, context, instance_id, **kwargs):
+ # NOTE(yamahata): name/description are ignored by register_image(),
+ # do so here
+ no_reboot = kwargs.get('no_reboot', False)
+
+ ec2_instance_id = instance_id
+ instance_id = ec2utils.ec2_id_to_id(ec2_instance_id)
+ instance = self.compute_api.get(context, instance_id)
+
+ # stop the instance if necessary
+ restart_instance = False
+ if not no_reboot:
+ state_description = instance['state_description']
+
+ # if the instance is in subtle state, refuse to proceed.
+ if state_description not in ('running', 'stopping', 'stopped'):
+ raise exception.InstanceNotRunning(instance_id=ec2_instance_id)
+
+ if state_description == 'running':
+ restart_instance = True
+ self.compute_api.stop(context, instance_id=instance_id)
+
+ # wait instance for really stopped
+ start_time = time.time()
+ while state_description != 'stopped':
+ time.sleep(1)
+ instance = self.compute_api.get(context, instance_id)
+ state_description = instance['state_description']
+ # NOTE(yamahata): timeout and error. 1 hour for now for safety.
+ # Is it too short/long?
+ # Or is there any better way?
+ timeout = 1 * 60 * 60 * 60
+ if time.time() > start_time + timeout:
+ raise exception.ApiError(
+ _('Couldn\'t stop instance with in %d sec') % timeout)
+
+ src_image = self._get_image(context, instance['image_ref'])
+ properties = src_image['properties']
+ if instance['root_device_name']:
+ properties['root_device_name'] = instance['root_device_name']
+
+ mapping = []
+ bdms = db.block_device_mapping_get_all_by_instance(context,
+ instance_id)
+ for bdm in bdms:
+ if bdm.no_device:
+ continue
+ m = {}
+ for attr in ('device_name', 'snapshot_id', 'volume_id',
+ 'volume_size', 'delete_on_termination', 'no_device',
+ 'virtual_name'):
+ val = getattr(bdm, attr)
+ if val is not None:
+ m[attr] = val
+
+ volume_id = m.get('volume_id')
+ if m.get('snapshot_id') and volume_id:
+ # create snapshot based on volume_id
+ vol = self.volume_api.get(context, volume_id=volume_id)
+ # NOTE(yamahata): Should we wait for snapshot creation?
+ # Linux LVM snapshot creation completes in
+ # short time, it doesn't matter for now.
+ snapshot = self.volume_api.create_snapshot_force(
+ context, volume_id=volume_id, name=vol['display_name'],
+ description=vol['display_description'])
+ m['snapshot_id'] = snapshot['id']
+ del m['volume_id']
+
+ if m:
+ mapping.append(m)
+
+ for m in _properties_get_mappings(properties):
+ virtual_name = m['virtual']
+ if virtual_name in ('ami', 'root'):
+ continue
+
+ assert (virtual_name == 'swap' or
+ virtual_name.startswith('ephemeral'))
+ device_name = m['device']
+ if device_name in [b['device_name'] for b in mapping
+ if not b.get('no_device', False)]:
+ continue
+
+ # NOTE(yamahata): swap and ephemeral devices are specified in
+ # AMI, but disabled for this instance by user.
+ # So disable those device by no_device.
+ mapping.append({'device_name': device_name, 'no_device': True})
+
+ if mapping:
+ properties['block_device_mapping'] = mapping
+
+ for attr in ('status', 'location', 'id'):
+ src_image.pop(attr, None)
+
+ image_id = self._register_image(context, src_image)
+
+ if restart_instance:
+ self.compute_api.start(context, instance_id=instance_id)
+
+ return {'imageId': image_id}
diff --git a/nova/api/ec2/ec2utils.py b/nova/api/ec2/ec2utils.py
index 222e1de1e..bae1e0ee5 100644
--- a/nova/api/ec2/ec2utils.py
+++ b/nova/api/ec2/ec2utils.py
@@ -34,6 +34,17 @@ def id_to_ec2_id(instance_id, template='i-%08x'):
return template % instance_id
+def id_to_ec2_snap_id(instance_id):
+ """Convert an snapshot ID (int) to an ec2 snapshot ID
+ (snap-[base 16 number])"""
+ return id_to_ec2_id(instance_id, 'snap-%08x')
+
+
+def id_to_ec2_vol_id(instance_id):
+ """Convert an volume ID (int) to an ec2 volume ID (vol-[base 16 number])"""
+ return id_to_ec2_id(instance_id, 'vol-%08x')
+
+
_c2u = re.compile('(((?<=[a-z])[A-Z])|([A-Z](?![A-Z]|$)))')
@@ -124,3 +135,32 @@ 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/ec2/metadatarequesthandler.py b/nova/api/ec2/metadatarequesthandler.py
index b70266a20..1dc275c90 100644
--- a/nova/api/ec2/metadatarequesthandler.py
+++ b/nova/api/ec2/metadatarequesthandler.py
@@ -35,6 +35,9 @@ FLAGS = flags.FLAGS
class MetadataRequestHandler(wsgi.Application):
"""Serve metadata from the EC2 API."""
+ def __init__(self):
+ self.cc = cloud.CloudController()
+
def print_data(self, data):
if isinstance(data, dict):
output = ''
@@ -68,12 +71,11 @@ class MetadataRequestHandler(wsgi.Application):
@webob.dec.wsgify(RequestClass=wsgi.Request)
def __call__(self, req):
- cc = cloud.CloudController()
remote_address = req.remote_addr
if FLAGS.use_forwarded_for:
remote_address = req.headers.get('X-Forwarded-For', remote_address)
try:
- meta_data = cc.get_metadata(remote_address)
+ meta_data = self.cc.get_metadata(remote_address)
except Exception:
LOG.exception(_('Failed to get metadata for ip: %s'),
remote_address)
diff --git a/nova/api/openstack/__init__.py b/nova/api/openstack/__init__.py
index f24017df0..e87d7c754 100644
--- a/nova/api/openstack/__init__.py
+++ b/nova/api/openstack/__init__.py
@@ -125,6 +125,10 @@ class APIRouter(base_wsgi.Router):
collection={'detail': 'GET'},
member=self.server_members)
+ mapper.resource("ip", "ips", controller=ips.create_resource(version),
+ parent_resource=dict(member_name='server',
+ collection_name='servers'))
+
mapper.resource("image", "images",
controller=images.create_resource(version),
collection={'detail': 'GET'})
@@ -144,9 +148,6 @@ class APIRouterV10(APIRouter):
def _setup_routes(self, mapper):
super(APIRouterV10, self)._setup_routes(mapper, '1.0')
- mapper.resource("image", "images",
- controller=images.create_resource('1.0'),
- collection={'detail': 'GET'})
mapper.resource("shared_ip_group", "shared_ip_groups",
collection={'detail': 'GET'},
@@ -157,11 +158,6 @@ class APIRouterV10(APIRouter):
parent_resource=dict(member_name='server',
collection_name='servers'))
- mapper.resource("ip", "ips", controller=ips.create_resource(),
- collection=dict(public='GET', private='GET'),
- parent_resource=dict(member_name='server',
- collection_name='servers'))
-
class APIRouterV11(APIRouter):
"""Define routes specific to OpenStack API V1.1."""
diff --git a/nova/api/openstack/accounts.py b/nova/api/openstack/accounts.py
index 0dcd37217..e3201b14f 100644
--- a/nova/api/openstack/accounts.py
+++ b/nova/api/openstack/accounts.py
@@ -87,8 +87,8 @@ def create_resource():
},
}
- serializers = {
+ body_serializers = {
'application/xml': wsgi.XMLDictSerializer(metadata=metadata),
}
-
- return wsgi.Resource(Controller(), serializers=serializers)
+ serializer = wsgi.ResponseSerializer(body_serializers)
+ return wsgi.Resource(Controller(), serializer=serializer)
diff --git a/nova/api/openstack/backup_schedules.py b/nova/api/openstack/backup_schedules.py
index 71a14d4ce..3e95aedf3 100644
--- a/nova/api/openstack/backup_schedules.py
+++ b/nova/api/openstack/backup_schedules.py
@@ -34,20 +34,20 @@ class Controller(object):
def __init__(self):
pass
- def index(self, req, server_id):
+ def index(self, req, server_id, **kwargs):
""" Returns the list of backup schedules for a given instance """
return faults.Fault(exc.HTTPNotImplemented())
- def show(self, req, server_id, id):
+ def show(self, req, server_id, id, **kwargs):
""" Returns a single backup schedule for a given instance """
return faults.Fault(exc.HTTPNotImplemented())
- def create(self, req, server_id, body):
+ def create(self, req, server_id, **kwargs):
""" No actual update method required, since the existing API allows
both create and update through a POST """
return faults.Fault(exc.HTTPNotImplemented())
- def delete(self, req, server_id, id):
+ def delete(self, req, server_id, id, **kwargs):
""" Deletes an existing backup schedule """
return faults.Fault(exc.HTTPNotImplemented())
@@ -59,9 +59,10 @@ def create_resource():
},
}
- serializers = {
+ body_serializers = {
'application/xml': wsgi.XMLDictSerializer(xmlns=wsgi.XMLNS_V10,
metadata=metadata),
}
- return wsgi.Resource(Controller(), serializers=serializers)
+ serializer = wsgi.ResponseSerializer(body_serializers)
+ return wsgi.Resource(Controller(), serializer=serializer)
diff --git a/nova/api/openstack/common.py b/nova/api/openstack/common.py
index 9aa384f33..8e12ce0c0 100644
--- a/nova/api/openstack/common.py
+++ b/nova/api/openstack/common.py
@@ -133,14 +133,32 @@ def get_id_from_href(href):
return int(urlparse(href).path.split('/')[-1])
except:
LOG.debug(_("Error extracting id from href: %s") % href)
- raise webob.exc.HTTPBadRequest(_('could not parse id from href'))
+ raise ValueError(_('could not parse id from href'))
-def remove_version_from_href(base_url):
- """Removes the api version from the href.
+def remove_version_from_href(href):
+ """Removes the first api version from the href.
Given: 'http://www.nova.com/v1.1/123'
Returns: 'http://www.nova.com/123'
+ Given: 'http://www.nova.com/v1.1'
+ Returns: 'http://www.nova.com'
+
"""
- return base_url.rsplit('/', 1).pop(0)
+ try:
+ #removes the first instance that matches /v#.#/
+ new_href = re.sub(r'[/][v][0-9]+\.[0-9]+[/]', '/', href, 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')
+ raise ValueError(msg)
+
+ if new_href == href:
+ msg = _('href does not contain version')
+ raise ValueError(msg)
+ return new_href
diff --git a/nova/api/openstack/consoles.py b/nova/api/openstack/consoles.py
index bccf04d8f..7a43fba96 100644
--- a/nova/api/openstack/consoles.py
+++ b/nova/api/openstack/consoles.py
@@ -90,14 +90,4 @@ class Controller(object):
def create_resource():
- metadata = {
- 'attributes': {
- 'console': [],
- },
- }
-
- serializers = {
- 'application/xml': wsgi.XMLDictSerializer(metadata=metadata),
- }
-
- return wsgi.Resource(Controller(), serializers=serializers)
+ return wsgi.Resource(Controller())
diff --git a/nova/api/openstack/contrib/floating_ips.py b/nova/api/openstack/contrib/floating_ips.py
index b27336574..b4a211857 100644
--- a/nova/api/openstack/contrib/floating_ips.py
+++ b/nova/api/openstack/contrib/floating_ips.py
@@ -78,7 +78,7 @@ class FloatingIPController(object):
return _translate_floating_ips_view(floating_ips)
- def create(self, req, body):
+ def create(self, req):
context = req.environ['nova.context']
try:
@@ -124,7 +124,7 @@ class FloatingIPController(object):
"floating_ip": floating_ip,
"fixed_ip": fixed_ip}}
- def disassociate(self, req, id, body):
+ def disassociate(self, req, id):
""" POST /floating_ips/{id}/disassociate """
context = req.environ['nova.context']
floating_ip = self.network_api.get_floating_ip(context, id)
diff --git a/nova/api/openstack/contrib/multinic.py b/nova/api/openstack/contrib/multinic.py
new file mode 100644
index 000000000..841061721
--- /dev/null
+++ b/nova/api/openstack/contrib/multinic.py
@@ -0,0 +1,125 @@
+# Copyright 2011 OpenStack LLC.
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+"""The multinic extension."""
+
+from webob import exc
+
+from nova import compute
+from nova import log as logging
+from nova.api.openstack import extensions
+from nova.api.openstack import faults
+
+
+LOG = logging.getLogger("nova.api.multinic")
+
+
+# Note: The class name is as it has to be for this to be loaded as an
+# extension--only first character capitalized.
+class Multinic(extensions.ExtensionDescriptor):
+ """The multinic extension.
+
+ Exposes addFixedIp and removeFixedIp actions on servers.
+
+ """
+
+ def __init__(self, *args, **kwargs):
+ """Initialize the extension.
+
+ Gets a compute.API object so we can call the back-end
+ add_fixed_ip() and remove_fixed_ip() methods.
+ """
+
+ super(Multinic, self).__init__(*args, **kwargs)
+ self.compute_api = compute.API()
+
+ def get_name(self):
+ """Return the extension name, as required by contract."""
+
+ return "Multinic"
+
+ def get_alias(self):
+ """Return the extension alias, as required by contract."""
+
+ return "NMN"
+
+ def get_description(self):
+ """Return the extension description, as required by contract."""
+
+ return "Multiple network support"
+
+ def get_namespace(self):
+ """Return the namespace, as required by contract."""
+
+ return "http://docs.openstack.org/ext/multinic/api/v1.1"
+
+ def get_updated(self):
+ """Return the last updated timestamp, as required by contract."""
+
+ return "2011-06-09T00:00:00+00:00"
+
+ def get_actions(self):
+ """Return the actions the extension adds, as required by contract."""
+
+ actions = []
+
+ # Add the add_fixed_ip action
+ act = extensions.ActionExtension("servers", "addFixedIp",
+ self._add_fixed_ip)
+ actions.append(act)
+
+ # Add the remove_fixed_ip action
+ act = extensions.ActionExtension("servers", "removeFixedIp",
+ self._remove_fixed_ip)
+ actions.append(act)
+
+ return actions
+
+ def _add_fixed_ip(self, input_dict, req, id):
+ """Adds an IP on a given network to an instance."""
+
+ try:
+ # Validate the input entity
+ if 'networkId' not in input_dict['addFixedIp']:
+ LOG.exception(_("Missing 'networkId' argument for addFixedIp"))
+ return faults.Fault(exc.HTTPUnprocessableEntity())
+
+ # Add the fixed IP
+ network_id = input_dict['addFixedIp']['networkId']
+ self.compute_api.add_fixed_ip(req.environ['nova.context'], id,
+ network_id)
+ except Exception, e:
+ LOG.exception(_("Error in addFixedIp %s"), e)
+ return faults.Fault(exc.HTTPBadRequest())
+ return exc.HTTPAccepted()
+
+ def _remove_fixed_ip(self, input_dict, req, id):
+ """Removes an IP from an instance."""
+
+ try:
+ # Validate the input entity
+ if 'address' not in input_dict['removeFixedIp']:
+ LOG.exception(_("Missing 'address' argument for "
+ "removeFixedIp"))
+ return faults.Fault(exc.HTTPUnprocessableEntity())
+
+ # Remove the fixed IP
+ address = input_dict['removeFixedIp']['address']
+ self.compute_api.remove_fixed_ip(req.environ['nova.context'], id,
+ address)
+ except Exception, e:
+ LOG.exception(_("Error in removeFixedIp %s"), e)
+ return faults.Fault(exc.HTTPBadRequest())
+ return exc.HTTPAccepted()
diff --git a/nova/api/openstack/create_instance_helper.py b/nova/api/openstack/create_instance_helper.py
index 1066713a3..2654e3c40 100644
--- a/nova/api/openstack/create_instance_helper.py
+++ b/nova/api/openstack/create_instance_helper.py
@@ -289,7 +289,7 @@ class ServerXMLDeserializer(wsgi.XMLDeserializer):
"""Deserialize an xml-formatted server create request"""
dom = minidom.parseString(string)
server = self._extract_server(dom)
- return {'server': server}
+ return {'body': {'server': server}}
def _extract_server(self, node):
"""Marshal the server attribute of a parsed request"""
diff --git a/nova/api/openstack/flavors.py b/nova/api/openstack/flavors.py
index a21ff6cb2..6fab13147 100644
--- a/nova/api/openstack/flavors.py
+++ b/nova/api/openstack/flavors.py
@@ -85,8 +85,10 @@ def create_resource(version='1.0'):
'1.1': wsgi.XMLNS_V11,
}[version]
- serializers = {
+ body_serializers = {
'application/xml': wsgi.XMLDictSerializer(xmlns=xmlns),
}
- return wsgi.Resource(controller, serializers=serializers)
+ serializer = wsgi.ResponseSerializer(body_serializers)
+
+ return wsgi.Resource(controller, serializer=serializer)
diff --git a/nova/api/openstack/image_metadata.py b/nova/api/openstack/image_metadata.py
index 638b1ec15..4f33844fa 100644
--- a/nova/api/openstack/image_metadata.py
+++ b/nova/api/openstack/image_metadata.py
@@ -160,8 +160,9 @@ class ImageMetadataXMLSerializer(wsgi.XMLDictSerializer):
def create_resource():
- serializers = {
+ body_serializers = {
'application/xml': ImageMetadataXMLSerializer(),
}
+ serializer = wsgi.ResponseSerializer(body_serializers)
- return wsgi.Resource(Controller(), serializers=serializers)
+ return wsgi.Resource(Controller(), serializer=serializer)
diff --git a/nova/api/openstack/images.py b/nova/api/openstack/images.py
index bde9507c8..d0317583e 100644
--- a/nova/api/openstack/images.py
+++ b/nova/api/openstack/images.py
@@ -13,6 +13,7 @@
# License for the specific language governing permissions and limitations
# under the License.
+import urlparse
import os.path
import webob.exc
@@ -23,10 +24,10 @@ from nova import exception
from nova import flags
import nova.image
from nova import log
-from nova import utils
from nova.api.openstack import common
from nova.api.openstack import faults
from nova.api.openstack import image_metadata
+from nova.api.openstack import servers
from nova.api.openstack.views import images as images_view
from nova.api.openstack import wsgi
@@ -246,13 +247,23 @@ class ControllerV11(Controller):
msg = _("Expected serverRef attribute on server entity.")
raise webob.exc.HTTPBadRequest(explanation=msg)
- head, tail = os.path.split(server_ref)
-
- if head and head != os.path.join(req.application_url, 'servers'):
+ if not server_ref.startswith('http'):
+ return server_ref
+
+ passed = urlparse.urlparse(server_ref)
+ expected = urlparse.urlparse(req.application_url)
+ version = expected.path.split('/')[1]
+ expected_prefix = "/%s/servers/" % version
+ _empty, _sep, server_id = passed.path.partition(expected_prefix)
+ scheme_ok = passed.scheme == expected.scheme
+ host_ok = passed.hostname == expected.hostname
+ port_ok = (passed.port == expected.port or
+ passed.port == FLAGS.osapi_port)
+ if not (scheme_ok and port_ok and host_ok and server_id):
msg = _("serverRef must match request url")
raise webob.exc.HTTPBadRequest(explanation=msg)
- return tail
+ return server_id
def _get_extra_properties(self, req, data):
server_ref = data['image']['serverRef']
@@ -264,59 +275,99 @@ class ControllerV11(Controller):
class ImageXMLSerializer(wsgi.XMLDictSerializer):
- metadata = {
- "attributes": {
- "image": ["id", "name", "updated", "created", "status",
- "serverId", "progress", "serverRef"],
- "link": ["rel", "type", "href"],
- },
- }
-
xmlns = wsgi.XMLNS_V11
def __init__(self):
self.metadata_serializer = image_metadata.ImageMetadataXMLSerializer()
def _image_to_xml(self, xml_doc, image):
- try:
- metadata = image.pop('metadata').items()
- except Exception:
- LOG.debug(_("Image object missing metadata attribute"))
- metadata = {}
-
- node = self._to_xml_node(xml_doc, self.metadata, 'image', image)
- metadata_node = self.metadata_serializer.meta_list_to_xml(xml_doc,
- metadata)
- node.appendChild(metadata_node)
- return node
-
- def _image_list_to_xml(self, xml_doc, images):
+ image_node = xml_doc.createElement('image')
+ image_node.setAttribute('id', str(image['id']))
+ image_node.setAttribute('name', image['name'])
+ link_nodes = self._create_link_nodes(xml_doc,
+ image['links'])
+ for link_node in link_nodes:
+ image_node.appendChild(link_node)
+ return image_node
+
+ def _image_to_xml_detailed(self, xml_doc, image):
+ image_node = xml_doc.createElement('image')
+ self._add_image_attributes(image_node, image)
+
+ if 'server' in image:
+ server_node = self._create_server_node(xml_doc, image['server'])
+ image_node.appendChild(server_node)
+
+ metadata = image.get('metadata', {}).items()
+ if len(metadata) > 0:
+ metadata_node = self._create_metadata_node(xml_doc, metadata)
+ image_node.appendChild(metadata_node)
+
+ link_nodes = self._create_link_nodes(xml_doc,
+ image['links'])
+ for link_node in link_nodes:
+ image_node.appendChild(link_node)
+
+ return image_node
+
+ def _add_image_attributes(self, node, image):
+ node.setAttribute('id', str(image['id']))
+ node.setAttribute('name', image['name'])
+ node.setAttribute('created', image['created'])
+ node.setAttribute('updated', image['updated'])
+ node.setAttribute('status', image['status'])
+ if 'progress' in image:
+ node.setAttribute('progress', str(image['progress']))
+
+ def _create_metadata_node(self, xml_doc, metadata):
+ return self.metadata_serializer.meta_list_to_xml(xml_doc, metadata)
+
+ def _create_server_node(self, xml_doc, server):
+ server_node = xml_doc.createElement('server')
+ server_node.setAttribute('id', str(server['id']))
+ link_nodes = self._create_link_nodes(xml_doc,
+ server['links'])
+ for link_node in link_nodes:
+ server_node.appendChild(link_node)
+ return server_node
+
+ def _image_list_to_xml(self, xml_doc, images, detailed):
container_node = xml_doc.createElement('images')
+ if detailed:
+ image_to_xml = self._image_to_xml_detailed
+ else:
+ image_to_xml = self._image_to_xml
+
for image in images:
- item_node = self._image_to_xml(xml_doc, image)
+ item_node = image_to_xml(xml_doc, image)
container_node.appendChild(item_node)
return container_node
- def _image_to_xml_string(self, image):
- xml_doc = minidom.Document()
- item_node = self._image_to_xml(xml_doc, image)
- self._add_xmlns(item_node)
- return item_node.toprettyxml(indent=' ')
-
- def _image_list_to_xml_string(self, images):
+ def index(self, images_dict):
xml_doc = minidom.Document()
- container_node = self._image_list_to_xml(xml_doc, images)
- self._add_xmlns(container_node)
- return container_node.toprettyxml(indent=' ')
+ node = self._image_list_to_xml(xml_doc,
+ images_dict['images'],
+ detailed=False)
+ return self.to_xml_string(node, True)
def detail(self, images_dict):
- return self._image_list_to_xml_string(images_dict['images'])
+ xml_doc = minidom.Document()
+ node = self._image_list_to_xml(xml_doc,
+ images_dict['images'],
+ detailed=True)
+ return self.to_xml_string(node, True)
def show(self, image_dict):
- return self._image_to_xml_string(image_dict['image'])
+ xml_doc = minidom.Document()
+ node = self._image_to_xml_detailed(xml_doc,
+ image_dict['image'])
+ return self.to_xml_string(node, True)
def create(self, image_dict):
- return self._image_to_xml_string(image_dict['image'])
+ xml_doc = minidom.Document()
+ node = self._image_to_xml_detailed(xml_doc,
+ image_dict['image'])
+ return self.to_xml_string(node, True)
def create_resource(version='1.0'):
@@ -338,8 +389,10 @@ def create_resource(version='1.0'):
'1.1': ImageXMLSerializer(),
}[version]
- serializers = {
+ body_serializers = {
'application/xml': xml_serializer,
}
- return wsgi.Resource(controller, serializers=serializers)
+ serializer = wsgi.ResponseSerializer(body_serializers)
+
+ return wsgi.Resource(controller, serializer=serializer)
diff --git a/nova/api/openstack/ips.py b/nova/api/openstack/ips.py
index 71646b6d3..1ebfdb831 100644
--- a/nova/api/openstack/ips.py
+++ b/nova/api/openstack/ips.py
@@ -23,6 +23,7 @@ import nova
from nova.api.openstack import faults
import nova.api.openstack.views.addresses
from nova.api.openstack import wsgi
+from nova import db
class Controller(object):
@@ -30,7 +31,6 @@ class Controller(object):
def __init__(self):
self.compute_api = nova.compute.API()
- self.builder = nova.api.openstack.views.addresses.ViewBuilderV10()
def _get_instance(self, req, server_id):
try:
@@ -40,29 +40,78 @@ class Controller(object):
return faults.Fault(exc.HTTPNotFound())
return instance
+ def create(self, req, server_id, body):
+ return faults.Fault(exc.HTTPNotImplemented())
+
+ def delete(self, req, server_id, id):
+ return faults.Fault(exc.HTTPNotImplemented())
+
+
+class ControllerV10(Controller):
+
def index(self, req, server_id):
instance = self._get_instance(req, server_id)
- return {'addresses': self.builder.build(instance)}
+ builder = nova.api.openstack.views.addresses.ViewBuilderV10()
+ return {'addresses': builder.build(instance)}
- def public(self, req, server_id):
+ def show(self, req, server_id, id):
instance = self._get_instance(req, server_id)
- return {'public': self.builder.build_public_parts(instance)}
+ builder = self._get_view_builder(req)
+ if id == 'private':
+ view = builder.build_private_parts(instance)
+ elif id == 'public':
+ view = builder.build_public_parts(instance)
+ else:
+ msg = _("Only private and public networks available")
+ return faults.Fault(exc.HTTPNotFound(explanation=msg))
- def private(self, req, server_id):
- instance = self._get_instance(req, server_id)
- return {'private': self.builder.build_private_parts(instance)}
+ return {id: view}
+
+ def _get_view_builder(self, req):
+ return nova.api.openstack.views.addresses.ViewBuilderV10()
+
+
+class ControllerV11(Controller):
+
+ def index(self, req, server_id):
+ context = req.environ['nova.context']
+ interfaces = self._get_virtual_interfaces(context, server_id)
+ networks = self._get_view_builder(req).build(interfaces)
+ return {'addresses': networks}
def show(self, req, server_id, id):
- return faults.Fault(exc.HTTPNotImplemented())
+ context = req.environ['nova.context']
+ interfaces = self._get_virtual_interfaces(context, server_id)
+ network = self._get_view_builder(req).build_network(interfaces, id)
- def create(self, req, server_id, body):
- return faults.Fault(exc.HTTPNotImplemented())
+ if network is None:
+ msg = _("Instance is not a member of specified network")
+ return faults.Fault(exc.HTTPNotFound(explanation=msg))
- def delete(self, req, server_id, id):
- return faults.Fault(exc.HTTPNotImplemented())
+ return network
+
+ def _get_virtual_interfaces(self, context, server_id):
+ try:
+ return db.api.virtual_interface_get_by_instance(context, server_id)
+ except nova.exception.InstanceNotFound:
+ msg = _("Instance does not exist")
+ raise exc.HTTPNotFound(explanation=msg)
+
+ def _get_view_builder(self, req):
+ return nova.api.openstack.views.addresses.ViewBuilderV11()
+
+
+def create_resource(version):
+ controller = {
+ '1.0': ControllerV10,
+ '1.1': ControllerV11,
+ }[version]()
+ xmlns = {
+ '1.0': wsgi.XMLNS_V10,
+ '1.1': wsgi.XMLNS_V11,
+ }[version]
-def create_resource():
metadata = {
'list_collections': {
'public': {'item_name': 'ip', 'item_key': 'addr'},
@@ -70,9 +119,10 @@ def create_resource():
},
}
- serializers = {
+ body_serializers = {
'application/xml': wsgi.XMLDictSerializer(metadata=metadata,
- xmlns=wsgi.XMLNS_V10),
+ xmlns=xmlns),
}
+ serializer = wsgi.ResponseSerializer(body_serializers)
- return wsgi.Resource(Controller(), serializers=serializers)
+ return wsgi.Resource(controller, serializer=serializer)
diff --git a/nova/api/openstack/limits.py b/nova/api/openstack/limits.py
index fede96e33..bc76547d8 100644
--- a/nova/api/openstack/limits.py
+++ b/nova/api/openstack/limits.py
@@ -31,8 +31,8 @@ from collections import defaultdict
from webob.dec import wsgify
from nova import quota
+from nova import utils
from nova import wsgi as base_wsgi
-from nova import wsgi
from nova.api.openstack import common
from nova.api.openstack import faults
from nova.api.openstack.views import limits as limits_views
@@ -97,12 +97,14 @@ def create_resource(version='1.0'):
},
}
- serializers = {
+ body_serializers = {
'application/xml': wsgi.XMLDictSerializer(xmlns=xmlns,
metadata=metadata),
}
- return wsgi.Resource(controller, serializers=serializers)
+ serializer = wsgi.ResponseSerializer(body_serializers)
+
+ return wsgi.Resource(controller, serializer=serializer)
class Limit(object):
@@ -117,6 +119,8 @@ class Limit(object):
60 * 60 * 24: "DAY",
}
+ UNIT_MAP = dict([(v, k) for k, v in UNITS.items()])
+
def __init__(self, verb, uri, regex, value, unit):
"""
Initialize a new `Limit`.
@@ -222,16 +226,30 @@ class RateLimitingMiddleware(base_wsgi.Middleware):
is stored in memory for this implementation.
"""
- def __init__(self, application, limits=None):
+ def __init__(self, application, limits=None, limiter=None, **kwargs):
"""
Initialize new `RateLimitingMiddleware`, which wraps the given WSGI
application and sets up the given limits.
@param application: WSGI application to wrap
- @param limits: List of dictionaries describing limits
+ @param limits: String describing limits
+ @param limiter: String identifying class for representing limits
+
+ Other parameters are passed to the constructor for the limiter.
"""
base_wsgi.Middleware.__init__(self, application)
- self._limiter = Limiter(limits or DEFAULT_LIMITS)
+
+ # Select the limiter class
+ if limiter is None:
+ limiter = Limiter
+ else:
+ limiter = utils.import_class(limiter)
+
+ # Parse the limits, if any are provided
+ if limits is not None:
+ limits = limiter.parse_limits(limits)
+
+ self._limiter = limiter(limits or DEFAULT_LIMITS, **kwargs)
@wsgify(RequestClass=wsgi.Request)
def __call__(self, req):
@@ -269,7 +287,7 @@ class Limiter(object):
Rate-limit checking class which handles limits in memory.
"""
- def __init__(self, limits):
+ def __init__(self, limits, **kwargs):
"""
Initialize the new `Limiter`.
@@ -278,6 +296,12 @@ class Limiter(object):
self.limits = copy.deepcopy(limits)
self.levels = defaultdict(lambda: copy.deepcopy(limits))
+ # Pick up any per-user limit information
+ for key, value in kwargs.items():
+ if key.startswith('user:'):
+ username = key[5:]
+ self.levels[username] = self.parse_limits(value)
+
def get_limits(self, username=None):
"""
Return the limits for a given user.
@@ -303,6 +327,66 @@ class Limiter(object):
return None, None
+ # Note: This method gets called before the class is instantiated,
+ # so this must be either a static method or a class method. It is
+ # used to develop a list of limits to feed to the constructor. We
+ # put this in the class so that subclasses can override the
+ # default limit parsing.
+ @staticmethod
+ def parse_limits(limits):
+ """
+ Convert a string into a list of Limit instances. This
+ implementation expects a semicolon-separated sequence of
+ parenthesized groups, where each group contains a
+ comma-separated sequence consisting of HTTP method,
+ user-readable URI, a URI reg-exp, an integer number of
+ requests which can be made, and a unit of measure. Valid
+ values for the latter are "SECOND", "MINUTE", "HOUR", and
+ "DAY".
+
+ @return: List of Limit instances.
+ """
+
+ # Handle empty limit strings
+ limits = limits.strip()
+ if not limits:
+ return []
+
+ # Split up the limits by semicolon
+ result = []
+ for group in limits.split(';'):
+ group = group.strip()
+ if group[:1] != '(' or group[-1:] != ')':
+ raise ValueError("Limit rules must be surrounded by "
+ "parentheses")
+ group = group[1:-1]
+
+ # Extract the Limit arguments
+ args = [a.strip() for a in group.split(',')]
+ if len(args) != 5:
+ raise ValueError("Limit rules must contain the following "
+ "arguments: verb, uri, regex, value, unit")
+
+ # Pull out the arguments
+ verb, uri, regex, value, unit = args
+
+ # Upper-case the verb
+ verb = verb.upper()
+
+ # Convert value--raises ValueError if it's not integer
+ value = int(value)
+
+ # Convert unit
+ unit = unit.upper()
+ if unit not in Limit.UNIT_MAP:
+ raise ValueError("Invalid units specified")
+ unit = Limit.UNIT_MAP[unit]
+
+ # Build a limit
+ result.append(Limit(verb, uri, regex, value, unit))
+
+ return result
+
class WsgiLimiter(object):
"""
@@ -386,3 +470,19 @@ class WsgiLimiterProxy(object):
return None, None
return resp.getheader("X-Wait-Seconds"), resp.read() or None
+
+ # Note: This method gets called before the class is instantiated,
+ # so this must be either a static method or a class method. It is
+ # used to develop a list of limits to feed to the constructor.
+ # This implementation returns an empty list, since all limit
+ # decisions are made by a remote server.
+ @staticmethod
+ def parse_limits(limits):
+ """
+ Ignore a limits string--simply doesn't apply for the limit
+ proxy.
+
+ @return: Empty list.
+ """
+
+ return []
diff --git a/nova/api/openstack/server_metadata.py b/nova/api/openstack/server_metadata.py
index 8a314de22..3b9169f81 100644
--- a/nova/api/openstack/server_metadata.py
+++ b/nova/api/openstack/server_metadata.py
@@ -123,8 +123,10 @@ class Controller(object):
def create_resource():
- serializers = {
+ body_serializers = {
'application/xml': wsgi.XMLDictSerializer(xmlns=wsgi.XMLNS_V11),
}
- return wsgi.Resource(Controller(), serializers=serializers)
+ serializer = wsgi.ResponseSerializer(body_serializers)
+
+ return wsgi.Resource(Controller(), serializer=serializer)
diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py
index eacc2109f..93f8e832c 100644
--- a/nova/api/openstack/servers.py
+++ b/nova/api/openstack/servers.py
@@ -19,6 +19,7 @@ import traceback
from webob import exc
from nova import compute
+from nova import db
from nova import exception
from nova import flags
from nova import log as logging
@@ -62,7 +63,7 @@ class Controller(object):
return exc.HTTPBadRequest(explanation=str(err))
return servers
- def _get_view_builder(self, req):
+ def _build_view(self, req, instance, is_detail=False):
raise NotImplementedError()
def _limit_items(self, items, req):
@@ -88,8 +89,7 @@ class Controller(object):
fixed_ip=fixed_ip,
recurse_zones=recurse_zones)
limited_list = self._limit_items(instance_list, req)
- builder = self._get_view_builder(req)
- servers = [builder.build(inst, is_detail)['server']
+ servers = [self._build_view(req, inst, is_detail)['server']
for inst in limited_list]
return dict(servers=servers)
@@ -99,20 +99,10 @@ class Controller(object):
try:
instance = self.compute_api.routing_get(
req.environ['nova.context'], id)
- builder = self._get_view_builder(req)
- return builder.build(instance, is_detail=True)
+ return self._build_view(req, instance, is_detail=True)
except exception.NotFound:
return faults.Fault(exc.HTTPNotFound())
- @scheduler_api.redirect_handler
- def delete(self, req, id):
- """ Destroys a server """
- try:
- self.compute_api.delete(req.environ['nova.context'], id)
- except exception.NotFound:
- return faults.Fault(exc.HTTPNotFound())
- return exc.HTTPAccepted()
-
def create(self, req, body):
""" Creates a new server for a given user """
extra_values = None
@@ -130,8 +120,7 @@ class Controller(object):
for key in ['instance_type', 'image_ref']:
inst[key] = extra_values[key]
- builder = self._get_view_builder(req)
- server = builder.build(inst, is_detail=True)
+ server = self._build_view(req, inst, is_detail=True)
server['server']['adminPass'] = extra_values['password']
return server
@@ -420,16 +409,25 @@ class Controller(object):
class ControllerV10(Controller):
+ @scheduler_api.redirect_handler
+ def delete(self, req, id):
+ """ Destroys a server """
+ try:
+ self.compute_api.delete(req.environ['nova.context'], id)
+ except exception.NotFound:
+ return faults.Fault(exc.HTTPNotFound())
+ return exc.HTTPAccepted()
+
def _image_ref_from_req_data(self, data):
return data['server']['imageId']
def _flavor_id_from_req_data(self, data):
return data['server']['flavorId']
- def _get_view_builder(self, req):
- addresses_builder = nova.api.openstack.views.addresses.ViewBuilderV10()
- return nova.api.openstack.views.servers.ViewBuilderV10(
- addresses_builder)
+ def _build_view(self, req, instance, is_detail=False):
+ addresses = nova.api.openstack.views.addresses.ViewBuilderV10()
+ builder = nova.api.openstack.views.servers.ViewBuilderV10(addresses)
+ return builder.build(instance, is_detail=is_detail)
def _limit_items(self, items, req):
return common.limited(items, req)
@@ -482,6 +480,15 @@ class ControllerV10(Controller):
class ControllerV11(Controller):
+
+ @scheduler_api.redirect_handler
+ def delete(self, req, id):
+ """ Destroys a server """
+ try:
+ self.compute_api.delete(req.environ['nova.context'], id)
+ except exception.NotFound:
+ return faults.Fault(exc.HTTPNotFound())
+
def _image_ref_from_req_data(self, data):
return data['server']['imageRef']
@@ -489,16 +496,18 @@ class ControllerV11(Controller):
href = data['server']['flavorRef']
return common.get_id_from_href(href)
- def _get_view_builder(self, req):
+ def _build_view(self, req, instance, is_detail=False):
base_url = req.application_url
flavor_builder = nova.api.openstack.views.flavors.ViewBuilderV11(
base_url)
image_builder = nova.api.openstack.views.images.ViewBuilderV11(
base_url)
addresses_builder = nova.api.openstack.views.addresses.ViewBuilderV11()
- return nova.api.openstack.views.servers.ViewBuilderV11(
+ builder = nova.api.openstack.views.servers.ViewBuilderV11(
addresses_builder, flavor_builder, image_builder, base_url)
+ return builder.build(instance, is_detail=is_detail)
+
def _action_change_password(self, input_dict, req, id):
context = req.environ['nova.context']
if (not 'changePassword' in input_dict
@@ -597,6 +606,12 @@ class ControllerV11(Controller):
return self.helper._get_server_admin_password_new_style(server)
+class HeadersSerializer(wsgi.ResponseHeadersSerializer):
+
+ def delete(self, response, data):
+ response.status_int = 204
+
+
def create_resource(version='1.0'):
controller = {
'1.0': ControllerV10,
@@ -624,14 +639,18 @@ def create_resource(version='1.0'):
'1.1': wsgi.XMLNS_V11,
}[version]
- serializers = {
+ headers_serializer = HeadersSerializer()
+
+ body_serializers = {
'application/xml': wsgi.XMLDictSerializer(metadata=metadata,
xmlns=xmlns),
}
- deserializers = {
+ body_deserializers = {
'application/xml': helper.ServerXMLDeserializer(),
}
- return wsgi.Resource(controller, serializers=serializers,
- deserializers=deserializers)
+ serializer = wsgi.ResponseSerializer(body_serializers, headers_serializer)
+ deserializer = wsgi.RequestDeserializer(body_deserializers)
+
+ return wsgi.Resource(controller, deserializer, serializer)
diff --git a/nova/api/openstack/shared_ip_groups.py b/nova/api/openstack/shared_ip_groups.py
index 4f11f8dfb..cf2ddbabb 100644
--- a/nova/api/openstack/shared_ip_groups.py
+++ b/nova/api/openstack/shared_ip_groups.py
@@ -24,27 +24,27 @@ from nova.api.openstack import wsgi
class Controller(object):
""" The Shared IP Groups Controller for the Openstack API """
- def index(self, req):
+ def index(self, req, **kwargs):
""" Returns a list of Shared IP Groups for the user """
raise faults.Fault(exc.HTTPNotImplemented())
- def show(self, req, id):
+ def show(self, req, id, **kwargs):
""" Shows in-depth information on a specific Shared IP Group """
raise faults.Fault(exc.HTTPNotImplemented())
- def update(self, req, id, body):
+ def update(self, req, id, **kwargs):
""" You can't update a Shared IP Group """
raise faults.Fault(exc.HTTPNotImplemented())
- def delete(self, req, id):
+ def delete(self, req, id, **kwargs):
""" Deletes a Shared IP Group """
raise faults.Fault(exc.HTTPNotImplemented())
- def detail(self, req):
+ def detail(self, req, **kwargs):
""" Returns a complete list of Shared IP Groups """
raise faults.Fault(exc.HTTPNotImplemented())
- def create(self, req, body):
+ def create(self, req, **kwargs):
""" Creates a new Shared IP group """
raise faults.Fault(exc.HTTPNotImplemented())
diff --git a/nova/api/openstack/users.py b/nova/api/openstack/users.py
index 50975fc1f..6ae1eaf2a 100644
--- a/nova/api/openstack/users.py
+++ b/nova/api/openstack/users.py
@@ -105,8 +105,10 @@ def create_resource():
},
}
- serializers = {
+ body_serializers = {
'application/xml': wsgi.XMLDictSerializer(metadata=metadata),
}
- return wsgi.Resource(Controller(), serializers=serializers)
+ serializer = wsgi.ResponseSerializer(body_serializers)
+
+ return wsgi.Resource(Controller(), serializer=serializer)
diff --git a/nova/api/openstack/versions.py b/nova/api/openstack/versions.py
index 4c682302f..a634c3267 100644
--- a/nova/api/openstack/versions.py
+++ b/nova/api/openstack/versions.py
@@ -31,11 +31,12 @@ class Versions(wsgi.Resource):
}
}
- serializers = {
+ body_serializers = {
'application/xml': wsgi.XMLDictSerializer(metadata=metadata),
}
+ serializer = wsgi.ResponseSerializer(body_serializers)
- wsgi.Resource.__init__(self, None, serializers=serializers)
+ wsgi.Resource.__init__(self, None, serializer=serializer)
def dispatch(self, request, *args):
"""Respond to a request for all OpenStack API versions."""
diff --git a/nova/api/openstack/views/addresses.py b/nova/api/openstack/views/addresses.py
index b59eb4751..a242efa45 100644
--- a/nova/api/openstack/views/addresses.py
+++ b/nova/api/openstack/views/addresses.py
@@ -20,13 +20,14 @@ from nova.api.openstack import common
class ViewBuilder(object):
- ''' Models a server addresses response as a python dictionary.'''
+ """Models a server addresses response as a python dictionary."""
def build(self, inst):
raise NotImplementedError()
class ViewBuilderV10(ViewBuilder):
+
def build(self, inst):
private_ips = self.build_private_parts(inst)
public_ips = self.build_public_parts(inst)
@@ -40,11 +41,31 @@ class ViewBuilderV10(ViewBuilder):
class ViewBuilderV11(ViewBuilder):
- def build(self, inst):
- # TODO(tr3buchet) - this shouldn't be hard coded to 4...
- private_ips = utils.get_from_path(inst, 'fixed_ips/address')
- private_ips = [dict(version=4, addr=a) for a in private_ips]
- public_ips = utils.get_from_path(inst,
- 'fixed_ips/floating_ips/address')
- public_ips = [dict(version=4, addr=a) for a in public_ips]
- return dict(public=public_ips, private=private_ips)
+
+ def build(self, interfaces):
+ networks = {}
+ for interface in interfaces:
+ network_label = interface['network']['label']
+
+ if network_label not in networks:
+ networks[network_label] = []
+
+ networks[network_label].extend(self._extract_ipv4(interface))
+
+ return networks
+
+ def build_network(self, interfaces, network_label):
+ for interface in interfaces:
+ if interface['network']['label'] == network_label:
+ ips = self._extract_ipv4(interface)
+ return {network_label: list(ips)}
+ return None
+
+ def _extract_ipv4(self, interface):
+ for fixed_ip in interface['fixed_ips']:
+ yield self._build_ip_entity(fixed_ip['address'], 4)
+ for floating_ip in fixed_ip.get('floating_ips', []):
+ yield self._build_ip_entity(floating_ip['address'], 4)
+
+ def _build_ip_entity(self, address, version):
+ return {'addr': address, 'version': version}
diff --git a/nova/api/openstack/views/images.py b/nova/api/openstack/views/images.py
index 005341c62..873ce212a 100644
--- a/nova/api/openstack/views/images.py
+++ b/nova/api/openstack/views/images.py
@@ -98,7 +98,20 @@ class ViewBuilderV11(ViewBuilder):
def _build_server(self, image, image_obj):
try:
- image['serverRef'] = image_obj['properties']['instance_ref']
+ serverRef = image_obj['properties']['instance_ref']
+ image['server'] = {
+ "id": common.get_id_from_href(serverRef),
+ "links": [
+ {
+ "rel": "self",
+ "href": serverRef,
+ },
+ {
+ "rel": "bookmark",
+ "href": common.remove_version_from_href(serverRef),
+ },
+ ]
+ }
except KeyError:
return
@@ -108,18 +121,21 @@ class ViewBuilderV11(ViewBuilder):
href = self.generate_href(image_obj["id"])
bookmark = self.generate_bookmark(image_obj["id"])
+ image["links"] = [
+ {
+ "rel": "self",
+ "href": href,
+ },
+ {
+ "rel": "bookmark",
+ "href": bookmark,
+ },
+
+ ]
+
if detail:
image["metadata"] = image_obj.get("properties", {})
- image["links"] = [{
- "rel": "self",
- "href": href,
- },
- {
- "rel": "bookmark",
- "href": bookmark,
- }]
-
return image
def generate_bookmark(self, image_id):
diff --git a/nova/api/openstack/views/servers.py b/nova/api/openstack/views/servers.py
index 67fb6a84e..ab7e8da61 100644
--- a/nova/api/openstack/views/servers.py
+++ b/nova/api/openstack/views/servers.py
@@ -77,7 +77,6 @@ class ViewBuilder(object):
inst_dict = {
'id': inst['id'],
'name': inst['display_name'],
- 'addresses': self.addresses_builder.build(inst),
'status': power_mapping[inst.get('state')]}
ctxt = nova.context.get_admin_context()
@@ -98,10 +97,15 @@ class ViewBuilder(object):
self._build_image(inst_dict, inst)
self._build_flavor(inst_dict, inst)
+ self._build_addresses(inst_dict, inst)
inst_dict['uuid'] = inst['uuid']
return dict(server=inst_dict)
+ def _build_addresses(self, response, inst):
+ """Return the addresses sub-resource of a server."""
+ raise NotImplementedError()
+
def _build_image(self, response, inst):
"""Return the image sub-resource of a server."""
raise NotImplementedError()
@@ -128,6 +132,9 @@ class ViewBuilderV10(ViewBuilder):
if 'instance_type' in dict(inst):
response['flavorId'] = inst['instance_type']['flavorid']
+ def _build_addresses(self, response, inst):
+ response['addresses'] = self.addresses_builder.build(inst)
+
class ViewBuilderV11(ViewBuilder):
"""Model an Openstack API V1.0 server response."""
@@ -151,6 +158,10 @@ class ViewBuilderV11(ViewBuilder):
flavor_ref = self.flavor_builder.generate_href(flavor_id)
response["flavorRef"] = flavor_ref
+ def _build_addresses(self, response, inst):
+ interfaces = inst.get('virtual_interfaces', [])
+ response['addresses'] = self.addresses_builder.build(interfaces)
+
def _build_extra(self, response, inst):
self._build_links(response, inst)
diff --git a/nova/api/openstack/wsgi.py b/nova/api/openstack/wsgi.py
index 5b6e3cb1d..c3f841aa5 100644
--- a/nova/api/openstack/wsgi.py
+++ b/nova/api/openstack/wsgi.py
@@ -46,38 +46,51 @@ class Request(webob.Request):
"""
if not "Content-Type" in self.headers:
- raise exception.InvalidContentType(content_type=None)
+ return None
allowed_types = ("application/xml", "application/json")
content_type = self.content_type
if content_type not in allowed_types:
raise exception.InvalidContentType(content_type=content_type)
- else:
- return content_type
+ return content_type
-class TextDeserializer(object):
- """Custom request body deserialization based on controller action name."""
- def deserialize(self, datastring, action='default'):
- """Find local deserialization method and parse request body."""
+class ActionDispatcher(object):
+ """Maps method name to local methods through action name."""
+
+ def dispatch(self, *args, **kwargs):
+ """Find and call local method."""
+ action = kwargs.pop('action', 'default')
action_method = getattr(self, str(action), self.default)
- return action_method(datastring)
+ return action_method(*args, **kwargs)
- def default(self, datastring):
- """Default deserialization code should live here"""
+ def default(self, data):
raise NotImplementedError()
-class JSONDeserializer(TextDeserializer):
+class TextDeserializer(ActionDispatcher):
+ """Default request body deserialization"""
+
+ def deserialize(self, datastring, action='default'):
+ return self.dispatch(datastring, action=action)
def default(self, datastring):
+ return {}
+
+
+class JSONDeserializer(TextDeserializer):
+
+ def _from_json(self, datastring):
try:
return utils.loads(datastring)
except ValueError:
- raise exception.MalformedRequestBody(
- reason=_("malformed JSON in request body"))
+ msg = _("cannot understand JSON")
+ raise exception.MalformedRequestBody(reason=msg)
+
+ def default(self, datastring):
+ return {'body': self._from_json(datastring)}
class XMLDeserializer(TextDeserializer):
@@ -90,15 +103,15 @@ class XMLDeserializer(TextDeserializer):
super(XMLDeserializer, self).__init__()
self.metadata = metadata or {}
- def default(self, datastring):
+ def _from_xml(self, datastring):
plurals = set(self.metadata.get('plurals', {}))
try:
node = minidom.parseString(datastring).childNodes[0]
return {node.nodeName: self._from_xml_node(node, plurals)}
except expat.ExpatError:
- raise exception.MalformedRequestBody(
- reason=_("malformed XML in request body"))
+ msg = _("cannot understand XML")
+ raise exception.MalformedRequestBody(reason=msg)
def _from_xml_node(self, node, listnames):
"""Convert a minidom node to a simple Python type.
@@ -121,21 +134,32 @@ class XMLDeserializer(TextDeserializer):
listnames)
return result
+ def default(self, datastring):
+ return {'body': self._from_xml(datastring)}
+
+
+class RequestHeadersDeserializer(ActionDispatcher):
+ """Default request headers deserializer"""
+
+ def deserialize(self, request, action):
+ return self.dispatch(request, action=action)
+
+ def default(self, request):
+ return {}
+
class RequestDeserializer(object):
"""Break up a Request object into more useful pieces."""
- def __init__(self, deserializers=None):
- """
- :param deserializers: dictionary of content-type-specific deserializers
-
- """
- self.deserializers = {
+ def __init__(self, body_deserializers=None, headers_deserializer=None):
+ self.body_deserializers = {
'application/xml': XMLDeserializer(),
'application/json': JSONDeserializer(),
}
+ self.body_deserializers.update(body_deserializers or {})
- self.deserializers.update(deserializers or {})
+ self.headers_deserializer = headers_deserializer or \
+ RequestHeadersDeserializer()
def deserialize(self, request):
"""Extract necessary pieces of the request.
@@ -149,26 +173,42 @@ class RequestDeserializer(object):
action_args = self.get_action_args(request.environ)
action = action_args.pop('action', None)
- if request.method.lower() in ('post', 'put'):
- if len(request.body) == 0:
- action_args['body'] = None
- else:
- content_type = request.get_content_type()
- deserializer = self.get_deserializer(content_type)
-
- try:
- body = deserializer.deserialize(request.body, action)
- action_args['body'] = body
- except exception.InvalidContentType:
- action_args['body'] = None
+ action_args.update(self.deserialize_headers(request, action))
+ action_args.update(self.deserialize_body(request, action))
accept = self.get_expected_content_type(request)
return (action, action_args, accept)
- def get_deserializer(self, content_type):
+ def deserialize_headers(self, request, action):
+ return self.headers_deserializer.deserialize(request, action)
+
+ def deserialize_body(self, request, action):
+ try:
+ content_type = request.get_content_type()
+ except exception.InvalidContentType:
+ LOG.debug(_("Unrecognized Content-Type provided in request"))
+ return {}
+
+ if content_type is None:
+ LOG.debug(_("No Content-Type provided in request"))
+ return {}
+
+ if not len(request.body) > 0:
+ LOG.debug(_("Empty body provided in request"))
+ return {}
+
try:
- return self.deserializers[content_type]
+ deserializer = self.get_body_deserializer(content_type)
+ except exception.InvalidContentType:
+ LOG.debug(_("Unable to deserialize body as provided Content-Type"))
+ raise
+
+ return deserializer.deserialize(request.body, action)
+
+ def get_body_deserializer(self, content_type):
+ try:
+ return self.body_deserializers[content_type]
except (KeyError, TypeError):
raise exception.InvalidContentType(content_type=content_type)
@@ -195,20 +235,18 @@ class RequestDeserializer(object):
return args
-class DictSerializer(object):
- """Custom response body serialization based on controller action name."""
+class DictSerializer(ActionDispatcher):
+ """Default request body serialization"""
def serialize(self, data, action='default'):
- """Find local serialization method and encode response body."""
- action_method = getattr(self, str(action), self.default)
- return action_method(data)
+ return self.dispatch(data, action=action)
def default(self, data):
- """Default serialization code should live here"""
- raise NotImplementedError()
+ return ""
class JSONDictSerializer(DictSerializer):
+ """Default JSON request body serialization"""
def default(self, data):
return utils.dumps(data)
@@ -232,13 +270,21 @@ class XMLDictSerializer(DictSerializer):
doc = minidom.Document()
node = self._to_xml_node(doc, self.metadata, root_key, data[root_key])
- self._add_xmlns(node)
+ return self.to_xml_string(node)
- return node.toprettyxml(indent=' ', encoding='utf-8')
+ def to_xml_string(self, node, has_atom=False):
+ self._add_xmlns(node, has_atom)
+ return node.toprettyxml(indent=' ', encoding='UTF-8')
- def _add_xmlns(self, node):
+ #NOTE (ameade): the has_atom should be removed after all of the
+ # xml serializers and view builders have been updated to the current
+ # spec that required all responses include the xmlns:atom, the has_atom
+ # flag is to prevent current tests from breaking
+ def _add_xmlns(self, node, has_atom=False):
if self.xmlns is not None:
node.setAttribute('xmlns', self.xmlns)
+ if has_atom:
+ node.setAttribute('xmlns:atom', "http://www.w3.org/2005/Atom")
def _to_xml_node(self, doc, metadata, nodename, data):
"""Recursive method to convert data members to XML nodes."""
@@ -294,20 +340,38 @@ class XMLDictSerializer(DictSerializer):
result.appendChild(node)
return result
+ def _create_link_nodes(self, xml_doc, links):
+ link_nodes = []
+ for link in links:
+ link_node = xml_doc.createElement('atom:link')
+ link_node.setAttribute('rel', link['rel'])
+ link_node.setAttribute('href', link['href'])
+ link_nodes.append(link_node)
+ return link_nodes
+
+
+class ResponseHeadersSerializer(ActionDispatcher):
+ """Default response headers serialization"""
+
+ def serialize(self, response, data, action):
+ self.dispatch(response, data, action=action)
+
+ def default(self, response, data):
+ response.status_int = 200
+
class ResponseSerializer(object):
"""Encode the necessary pieces into a response object"""
- def __init__(self, serializers=None):
- """
- :param serializers: dictionary of content-type-specific serializers
-
- """
- self.serializers = {
+ def __init__(self, body_serializers=None, headers_serializer=None):
+ self.body_serializers = {
'application/xml': XMLDictSerializer(),
'application/json': JSONDictSerializer(),
}
- self.serializers.update(serializers or {})
+ self.body_serializers.update(body_serializers or {})
+
+ self.headers_serializer = headers_serializer or \
+ ResponseHeadersSerializer()
def serialize(self, response_data, content_type, action='default'):
"""Serialize a dict into a string and wrap in a wsgi.Request object.
@@ -317,16 +381,21 @@ class ResponseSerializer(object):
"""
response = webob.Response()
- response.headers['Content-Type'] = content_type
+ self.serialize_headers(response, response_data, action)
+ self.serialize_body(response, response_data, content_type, action)
+ return response
- serializer = self.get_serializer(content_type)
- response.body = serializer.serialize(response_data, action)
+ def serialize_headers(self, response, data, action):
+ self.headers_serializer.serialize(response, data, action)
- return response
+ def serialize_body(self, response, data, content_type, action):
+ response.headers['Content-Type'] = content_type
+ serializer = self.get_body_serializer(content_type)
+ response.body = serializer.serialize(data, action)
- def get_serializer(self, content_type):
+ def get_body_serializer(self, content_type):
try:
- return self.serializers[content_type]
+ return self.body_serializers[content_type]
except (KeyError, TypeError):
raise exception.InvalidContentType(content_type=content_type)
@@ -343,16 +412,18 @@ class Resource(wsgi.Application):
serialized by requested content type.
"""
- def __init__(self, controller, serializers=None, deserializers=None):
+ def __init__(self, controller, deserializer=None, serializer=None):
"""
:param controller: object that implement methods created by routes lib
- :param serializers: dict of content-type specific text serializers
- :param deserializers: dict of content-type specific text deserializers
+ :param deserializer: object that can serialize the output of a
+ controller into a webob response
+ :param serializer: object that can deserialize a webob request
+ into necessary pieces
"""
self.controller = controller
- self.serializer = ResponseSerializer(serializers)
- self.deserializer = RequestDeserializer(deserializers)
+ self.deserializer = deserializer or RequestDeserializer()
+ self.serializer = serializer or ResponseSerializer()
@webob.dec.wsgify(RequestClass=Request)
def __call__(self, request):
@@ -362,8 +433,7 @@ class Resource(wsgi.Application):
"url": request.url})
try:
- action, action_args, accept = self.deserializer.deserialize(
- request)
+ action, args, accept = self.deserializer.deserialize(request)
except exception.InvalidContentType:
msg = _("Unsupported Content-Type")
return webob.exc.HTTPBadRequest(explanation=msg)
@@ -371,11 +441,13 @@ class Resource(wsgi.Application):
msg = _("Malformed request body")
return faults.Fault(webob.exc.HTTPBadRequest(explanation=msg))
- action_result = self.dispatch(request, action, action_args)
+ action_result = self.dispatch(request, action, args)
#TODO(bcwaldon): find a more elegant way to pass through non-dict types
- if type(action_result) is dict:
- response = self.serializer.serialize(action_result, accept, action)
+ if type(action_result) is dict or action_result is None:
+ response = self.serializer.serialize(action_result,
+ accept,
+ action=action)
else:
response = action_result
@@ -394,4 +466,8 @@ class Resource(wsgi.Application):
"""Find action-spefic method on controller and call it."""
controller_method = getattr(self.controller, action)
- return controller_method(req=request, **action_args)
+ try:
+ return controller_method(req=request, **action_args)
+ except TypeError, exc:
+ LOG.debug(str(exc))
+ return webob.exc.HTTPBadRequest()
diff --git a/nova/api/openstack/zones.py b/nova/api/openstack/zones.py
index 8864f825b..2e02ec380 100644
--- a/nova/api/openstack/zones.py
+++ b/nova/api/openstack/zones.py
@@ -196,14 +196,15 @@ def create_resource(version):
},
}
- serializers = {
+ body_serializers = {
'application/xml': wsgi.XMLDictSerializer(xmlns=wsgi.XMLNS_V10,
metadata=metadata),
}
+ serializer = wsgi.ResponseSerializer(body_serializers)
- deserializers = {
+ body_deserializers = {
'application/xml': helper.ServerXMLDeserializer(),
}
+ deserializer = wsgi.RequestDeserializer(body_deserializers)
- return wsgi.Resource(controller, serializers=serializers,
- deserializers=deserializers)
+ return wsgi.Resource(controller, deserializer, serializer)
diff --git a/nova/compute/api.py b/nova/compute/api.py
index edd1a4d64..acafc7760 100644
--- a/nova/compute/api.py
+++ b/nova/compute/api.py
@@ -32,6 +32,7 @@ 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
@@ -217,6 +218,9 @@ class API(base.Base):
if reservation_id is None:
reservation_id = utils.generate_uid('r')
+ root_device_name = ec2utils.properties_root_device_name(
+ image['properties'])
+
base_options = {
'reservation_id': reservation_id,
'image_ref': image_href,
@@ -241,11 +245,61 @@ class API(base.Base):
'availability_zone': availability_zone,
'os_type': os_type,
'architecture': architecture,
- 'vm_mode': vm_mode}
+ 'vm_mode': vm_mode,
+ 'root_device_name': root_device_name}
+
+ return (num_instances, base_options, image)
+
+ def _update_image_block_device_mapping(self, elevated_context, 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):
+ LOG.debug(_("bdm %s"), bdm)
+
+ virtual_name = bdm['virtual']
+ if virtual_name == 'ami' or virtual_name == 'root':
+ continue
- return (num_instances, base_options)
+ assert (virtual_name == 'swap' or
+ virtual_name.startswith('ephemeral'))
+ values = {
+ 'instance_id': instance_id,
+ 'device_name': bdm['device'],
+ 'virtual_name': virtual_name, }
+ self.db.block_device_mapping_update_or_create(elevated_context,
+ values)
+
+ def _update_block_device_mapping(self, elevated_context, instance_id,
+ block_device_mapping):
+ """tell vm driver to attach volume at boot time by updating
+ BlockDeviceMapping
+ """
+ for bdm in block_device_mapping:
+ LOG.debug(_('bdm %s'), bdm)
+ assert 'device_name' in bdm
- def create_db_entry_for_new_instance(self, context, base_options,
+ values = {'instance_id': instance_id}
+ for key in ('device_name', 'delete_on_termination', 'virtual_name',
+ 'snapshot_id', 'volume_id', 'volume_size',
+ 'no_device'):
+ values[key] = bdm.get(key)
+
+ # NOTE(yamahata): NoDevice eliminates devices defined in image
+ # files by command line option.
+ # (--block-device-mapping)
+ if bdm.get('virtual_name') == 'NoDevice':
+ values['no_device'] = True
+ for k in ('delete_on_termination', 'volume_id',
+ 'snapshot_id', 'volume_id', 'volume_size',
+ 'virtual_name'):
+ values[k] = None
+
+ 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):
"""Create an entry in the DB for this new instance,
including any related table updates (such as security group,
@@ -278,23 +332,14 @@ class API(base.Base):
instance_id,
security_group_id)
- block_device_mapping = block_device_mapping or []
- # NOTE(yamahata)
- # tell vm driver to attach volume at boot time by updating
- # BlockDeviceMapping
- for bdm in block_device_mapping:
- LOG.debug(_('bdm %s'), bdm)
- assert 'device_name' in bdm
- values = {
- 'instance_id': instance_id,
- 'device_name': bdm['device_name'],
- 'delete_on_termination': bdm.get('delete_on_termination'),
- 'virtual_name': bdm.get('virtual_name'),
- 'snapshot_id': bdm.get('snapshot_id'),
- 'volume_id': bdm.get('volume_id'),
- 'volume_size': bdm.get('volume_size'),
- 'no_device': bdm.get('no_device')}
- self.db.block_device_mapping_create(elevated, values)
+ # BlockDeviceMapping table
+ self._update_image_block_device_mapping(elevated, instance_id,
+ image['properties'].get('mappings', []))
+ self._update_block_device_mapping(elevated, instance_id,
+ image['properties'].get('block_device_mapping', []))
+ # override via command line option
+ self._update_block_device_mapping(elevated, instance_id,
+ block_device_mapping)
# Set sane defaults if not specified
updates = {}
@@ -356,7 +401,7 @@ class API(base.Base):
"""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."""
- num_instances, base_options = self._check_create_parameters(
+ num_instances, base_options, image = self._check_create_parameters(
context, instance_type,
image_href, kernel_id, ramdisk_id,
min_count, max_count,
@@ -394,7 +439,7 @@ class API(base.Base):
Returns a list of instance dicts.
"""
- num_instances, base_options = self._check_create_parameters(
+ num_instances, base_options, image = self._check_create_parameters(
context, instance_type,
image_href, kernel_id, ramdisk_id,
min_count, max_count,
@@ -404,10 +449,11 @@ class API(base.Base):
injected_files, admin_password, zone_blob,
reservation_id)
+ block_device_mapping = block_device_mapping or []
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,
+ instance = self.create_db_entry_for_new_instance(context, image,
base_options, security_group,
block_device_mapping, num=num)
instances.append(instance)
@@ -901,8 +947,14 @@ class API(base.Base):
def add_fixed_ip(self, context, instance_id, network_id):
"""Add fixed_ip from specified network to given instance."""
self._cast_compute_message('add_fixed_ip_to_instance', context,
- instance_id,
- network_id)
+ instance_id,
+ params=dict(network_id=network_id))
+
+ @scheduler_api.reroute_compute("remove_fixed_ip")
+ def remove_fixed_ip(self, context, instance_id, address):
+ """Remove fixed_ip from specified network to given instance."""
+ self._cast_compute_message('remove_fixed_ip_from_instance', context,
+ instance_id, params=dict(address=address))
#TODO(tr3buchet): how to run this in the correct zone?
def add_network_to_project(self, context, project_id):
diff --git a/nova/compute/manager.py b/nova/compute/manager.py
index 5d21c9eb2..77ff9e317 100644
--- a/nova/compute/manager.py
+++ b/nova/compute/manager.py
@@ -54,7 +54,7 @@ from nova import rpc
from nova import utils
from nova import volume
from nova.compute import power_state
-from nova.notifier import api as notifier_api
+from nova.notifier import api as notifier
from nova.compute.utils import terminate_volumes
from nova.virt import driver
@@ -83,6 +83,10 @@ flags.DEFINE_integer('host_state_interval', 120,
LOG = logging.getLogger('nova.compute.manager')
+def publisher_id(host=None):
+ return notifier.publisher_id("compute", host)
+
+
def checks_instance_lock(function):
"""Decorator to prevent action against locked instances for non-admins."""
@functools.wraps(function)
@@ -181,7 +185,7 @@ class ComputeManager(manager.SchedulerDependentManager):
def get_console_pool_info(self, context, console_type):
return self.driver.get_console_pool_info(console_type)
- @exception.wrap_exception
+ @exception.wrap_exception(notifier=notifier, publisher_id=publisher_id())
def refresh_security_group_rules(self, context, security_group_id,
**kwargs):
"""Tell the virtualization driver to refresh security group rules.
@@ -191,7 +195,7 @@ class ComputeManager(manager.SchedulerDependentManager):
"""
return self.driver.refresh_security_group_rules(security_group_id)
- @exception.wrap_exception
+ @exception.wrap_exception(notifier=notifier, publisher_id=publisher_id())
def refresh_security_group_members(self, context,
security_group_id, **kwargs):
"""Tell the virtualization driver to refresh security group members.
@@ -201,7 +205,7 @@ class ComputeManager(manager.SchedulerDependentManager):
"""
return self.driver.refresh_security_group_members(security_group_id)
- @exception.wrap_exception
+ @exception.wrap_exception(notifier=notifier, publisher_id=publisher_id())
def refresh_provider_fw_rules(self, context, **_kwargs):
"""This call passes straight through to the virtualization driver."""
return self.driver.refresh_provider_fw_rules()
@@ -218,6 +222,17 @@ class ComputeManager(manager.SchedulerDependentManager):
for bdm in self.db.block_device_mapping_get_all_by_instance(
context, instance_id):
LOG.debug(_("setting up bdm %s"), bdm)
+
+ 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'))
+ continue
+
if ((bdm['snapshot_id'] is not None) and
(bdm['volume_id'] is None)):
# TODO(yamahata): default name and description
@@ -250,15 +265,6 @@ class ComputeManager(manager.SchedulerDependentManager):
block_device_mapping.append({'device_path': dev_path,
'mount_device':
bdm['device_name']})
- elif bdm['virtual_name'] is not None:
- # TODO(yamahata): ephemeral/swap device support
- LOG.debug(_('block_device_mapping: '
- 'ephemeral device is not supported yet'))
- else:
- # TODO(yamahata): NoDevice support
- assert bdm['no_device']
- LOG.debug(_('block_device_mapping: '
- 'no device is not supported yet'))
return block_device_mapping
@@ -319,10 +325,9 @@ class ComputeManager(manager.SchedulerDependentManager):
self._update_launched_at(context, instance_id)
self._update_state(context, instance_id)
usage_info = utils.usage_from_instance(instance)
- notifier_api.notify('compute.%s' % self.host,
- 'compute.instance.create',
- notifier_api.INFO,
- usage_info)
+ notifier.notify('compute.%s' % self.host,
+ 'compute.instance.create',
+ notifier.INFO, usage_info)
except exception.InstanceNotFound:
# FIXME(wwolf): We are just ignoring InstanceNotFound
# exceptions here in case the instance was immediately
@@ -330,11 +335,11 @@ class ComputeManager(manager.SchedulerDependentManager):
# be fixed once we have no-db-messaging
pass
- @exception.wrap_exception
+ @exception.wrap_exception(notifier=notifier, publisher_id=publisher_id())
def run_instance(self, context, instance_id, **kwargs):
self._run_instance(context, instance_id, **kwargs)
- @exception.wrap_exception
+ @exception.wrap_exception(notifier=notifier, publisher_id=publisher_id())
@checks_instance_lock
def start_instance(self, context, instance_id):
"""Starting an instance on this host."""
@@ -367,7 +372,7 @@ class ComputeManager(manager.SchedulerDependentManager):
if action_str == 'Terminating':
terminate_volumes(self.db, context, instance_id)
- @exception.wrap_exception
+ @exception.wrap_exception(notifier=notifier, publisher_id=publisher_id())
@checks_instance_lock
def terminate_instance(self, context, instance_id):
"""Terminate an instance on this host."""
@@ -377,19 +382,18 @@ class ComputeManager(manager.SchedulerDependentManager):
# TODO(ja): should we keep it in a terminated state for a bit?
self.db.instance_destroy(context, instance_id)
usage_info = utils.usage_from_instance(instance)
- notifier_api.notify('compute.%s' % self.host,
- 'compute.instance.delete',
- notifier_api.INFO,
- usage_info)
+ notifier.notify('compute.%s' % self.host,
+ 'compute.instance.delete',
+ notifier.INFO, usage_info)
- @exception.wrap_exception
+ @exception.wrap_exception(notifier=notifier, publisher_id=publisher_id())
@checks_instance_lock
def stop_instance(self, context, instance_id):
"""Stopping an instance on this host."""
self._shutdown_instance(context, instance_id, 'Stopping')
# instance state will be updated to stopped by _poll_instance_states()
- @exception.wrap_exception
+ @exception.wrap_exception(notifier=notifier, publisher_id=publisher_id())
@checks_instance_lock
def rebuild_instance(self, context, instance_id, **kwargs):
"""Destroy and re-make this instance.
@@ -419,12 +423,12 @@ class ComputeManager(manager.SchedulerDependentManager):
self._update_state(context, instance_id)
usage_info = utils.usage_from_instance(instance_ref,
image_ref=image_ref)
- notifier_api.notify('compute.%s' % self.host,
+ notifier.notify('compute.%s' % self.host,
'compute.instance.rebuild',
- notifier_api.INFO,
+ notifier.INFO,
usage_info)
- @exception.wrap_exception
+ @exception.wrap_exception(notifier=notifier, publisher_id=publisher_id())
@checks_instance_lock
def reboot_instance(self, context, instance_id):
"""Reboot an instance on this host."""
@@ -449,7 +453,7 @@ class ComputeManager(manager.SchedulerDependentManager):
self.driver.reboot(instance_ref)
self._update_state(context, instance_id)
- @exception.wrap_exception
+ @exception.wrap_exception(notifier=notifier, publisher_id=publisher_id())
def snapshot_instance(self, context, instance_id, image_id,
image_type='snapshot', backup_type=None,
rotation=None):
@@ -541,7 +545,7 @@ class ComputeManager(manager.SchedulerDependentManager):
LOG.debug(_("Deleting image %d" % image_id))
image_service.delete(context, image_id)
- @exception.wrap_exception
+ @exception.wrap_exception(notifier=notifier, publisher_id=publisher_id())
@checks_instance_lock
def set_admin_password(self, context, instance_id, new_pass=None):
"""Set the root/admin password for an instance on this host.
@@ -589,7 +593,7 @@ class ComputeManager(manager.SchedulerDependentManager):
time.sleep(1)
continue
- @exception.wrap_exception
+ @exception.wrap_exception(notifier=notifier, publisher_id=publisher_id())
@checks_instance_lock
def inject_file(self, context, instance_id, path, file_contents):
"""Write a file to the specified path in an instance on this host."""
@@ -607,7 +611,7 @@ class ComputeManager(manager.SchedulerDependentManager):
LOG.audit(msg)
self.driver.inject_file(instance_ref, path, file_contents)
- @exception.wrap_exception
+ @exception.wrap_exception(notifier=notifier, publisher_id=publisher_id())
@checks_instance_lock
def agent_update(self, context, instance_id, url, md5hash):
"""Update agent running on an instance on this host."""
@@ -625,7 +629,7 @@ class ComputeManager(manager.SchedulerDependentManager):
LOG.audit(msg)
self.driver.agent_update(instance_ref, url, md5hash)
- @exception.wrap_exception
+ @exception.wrap_exception(notifier=notifier, publisher_id=publisher_id())
@checks_instance_lock
def rescue_instance(self, context, instance_id):
"""Rescue an instance on this host."""
@@ -642,7 +646,7 @@ class ComputeManager(manager.SchedulerDependentManager):
self.driver.rescue(instance_ref, _update_state)
self._update_state(context, instance_id)
- @exception.wrap_exception
+ @exception.wrap_exception(notifier=notifier, publisher_id=publisher_id())
@checks_instance_lock
def unrescue_instance(self, context, instance_id):
"""Rescue an instance on this host."""
@@ -663,7 +667,7 @@ class ComputeManager(manager.SchedulerDependentManager):
"""Update instance state when async task completes."""
self._update_state(context, instance_id)
- @exception.wrap_exception
+ @exception.wrap_exception(notifier=notifier, publisher_id=publisher_id())
@checks_instance_lock
def confirm_resize(self, context, instance_id, migration_id):
"""Destroys the source instance."""
@@ -671,12 +675,12 @@ class ComputeManager(manager.SchedulerDependentManager):
instance_ref = self.db.instance_get(context, instance_id)
self.driver.destroy(instance_ref)
usage_info = utils.usage_from_instance(instance_ref)
- notifier_api.notify('compute.%s' % self.host,
+ notifier.notify('compute.%s' % self.host,
'compute.instance.resize.confirm',
- notifier_api.INFO,
+ notifier.INFO,
usage_info)
- @exception.wrap_exception
+ @exception.wrap_exception(notifier=notifier, publisher_id=publisher_id())
@checks_instance_lock
def revert_resize(self, context, instance_id, migration_id):
"""Destroys the new instance on the destination machine.
@@ -698,7 +702,7 @@ class ComputeManager(manager.SchedulerDependentManager):
'instance_id': instance_id, },
})
- @exception.wrap_exception
+ @exception.wrap_exception(notifier=notifier, publisher_id=publisher_id())
@checks_instance_lock
def finish_revert_resize(self, context, instance_id, migration_id):
"""Finishes the second half of reverting a resize.
@@ -723,12 +727,12 @@ class ComputeManager(manager.SchedulerDependentManager):
self.db.migration_update(context, migration_id,
{'status': 'reverted'})
usage_info = utils.usage_from_instance(instance_ref)
- notifier_api.notify('compute.%s' % self.host,
+ notifier.notify('compute.%s' % self.host,
'compute.instance.resize.revert',
- notifier_api.INFO,
+ notifier.INFO,
usage_info)
- @exception.wrap_exception
+ @exception.wrap_exception(notifier=notifier, publisher_id=publisher_id())
@checks_instance_lock
def prep_resize(self, context, instance_id, flavor_id):
"""Initiates the process of moving a running instance to another host.
@@ -766,12 +770,12 @@ class ComputeManager(manager.SchedulerDependentManager):
usage_info = utils.usage_from_instance(instance_ref,
new_instance_type=instance_type['name'],
new_instance_type_id=instance_type['id'])
- notifier_api.notify('compute.%s' % self.host,
+ notifier.notify('compute.%s' % self.host,
'compute.instance.resize.prep',
- notifier_api.INFO,
+ notifier.INFO,
usage_info)
- @exception.wrap_exception
+ @exception.wrap_exception(notifier=notifier, publisher_id=publisher_id())
@checks_instance_lock
def resize_instance(self, context, instance_id, migration_id):
"""Starts the migration of a running instance to another host."""
@@ -797,7 +801,7 @@ class ComputeManager(manager.SchedulerDependentManager):
'instance_id': instance_id,
'disk_info': disk_info}})
- @exception.wrap_exception
+ @exception.wrap_exception(notifier=notifier, publisher_id=publisher_id())
@checks_instance_lock
def finish_resize(self, context, instance_id, migration_id, disk_info):
"""Completes the migration process.
@@ -829,7 +833,7 @@ class ComputeManager(manager.SchedulerDependentManager):
self.db.migration_update(context, migration_id,
{'status': 'finished', })
- @exception.wrap_exception
+ @exception.wrap_exception(notifier=notifier, publisher_id=publisher_id())
@checks_instance_lock
def add_fixed_ip_to_instance(self, context, instance_id, network_id):
"""Calls network_api to add new fixed_ip to instance
@@ -841,7 +845,19 @@ class ComputeManager(manager.SchedulerDependentManager):
self.inject_network_info(context, instance_id)
self.reset_network(context, instance_id)
- @exception.wrap_exception
+ @exception.wrap_exception(notifier=notifier, publisher_id=publisher_id())
+ @checks_instance_lock
+ def remove_fixed_ip_from_instance(self, context, instance_id, address):
+ """Calls network_api to remove existing fixed_ip from instance
+ by injecting the altered network info and resetting
+ instance networking.
+ """
+ self.network_api.remove_fixed_ip_from_instance(context, instance_id,
+ address)
+ self.inject_network_info(context, instance_id)
+ self.reset_network(context, instance_id)
+
+ @exception.wrap_exception(notifier=notifier, publisher_id=publisher_id())
@checks_instance_lock
def pause_instance(self, context, instance_id):
"""Pause an instance on this host."""
@@ -858,7 +874,7 @@ class ComputeManager(manager.SchedulerDependentManager):
instance_id,
result))
- @exception.wrap_exception
+ @exception.wrap_exception(notifier=notifier, publisher_id=publisher_id())
@checks_instance_lock
def unpause_instance(self, context, instance_id):
"""Unpause a paused instance on this host."""
@@ -875,13 +891,13 @@ class ComputeManager(manager.SchedulerDependentManager):
instance_id,
result))
- @exception.wrap_exception
+ @exception.wrap_exception(notifier=notifier, publisher_id=publisher_id())
def set_host_enabled(self, context, instance_id=None, host=None,
enabled=None):
"""Sets the specified host's ability to accept new instances."""
return self.driver.set_host_enabled(host, enabled)
- @exception.wrap_exception
+ @exception.wrap_exception(notifier=notifier, publisher_id=publisher_id())
def get_diagnostics(self, context, instance_id):
"""Retrieve diagnostics for an instance on this host."""
instance_ref = self.db.instance_get(context, instance_id)
@@ -890,7 +906,7 @@ class ComputeManager(manager.SchedulerDependentManager):
context=context)
return self.driver.get_diagnostics(instance_ref)
- @exception.wrap_exception
+ @exception.wrap_exception(notifier=notifier, publisher_id=publisher_id())
@checks_instance_lock
def suspend_instance(self, context, instance_id):
"""Suspend the given instance."""
@@ -906,7 +922,7 @@ class ComputeManager(manager.SchedulerDependentManager):
instance_id,
result))
- @exception.wrap_exception
+ @exception.wrap_exception(notifier=notifier, publisher_id=publisher_id())
@checks_instance_lock
def resume_instance(self, context, instance_id):
"""Resume the given suspended instance."""
@@ -922,7 +938,7 @@ class ComputeManager(manager.SchedulerDependentManager):
instance_id,
result))
- @exception.wrap_exception
+ @exception.wrap_exception(notifier=notifier, publisher_id=publisher_id())
def lock_instance(self, context, instance_id):
"""Lock the given instance."""
context = context.elevated()
@@ -930,7 +946,7 @@ class ComputeManager(manager.SchedulerDependentManager):
LOG.debug(_('instance %s: locking'), instance_id, context=context)
self.db.instance_update(context, instance_id, {'locked': True})
- @exception.wrap_exception
+ @exception.wrap_exception(notifier=notifier, publisher_id=publisher_id())
def unlock_instance(self, context, instance_id):
"""Unlock the given instance."""
context = context.elevated()
@@ -938,7 +954,7 @@ class ComputeManager(manager.SchedulerDependentManager):
LOG.debug(_('instance %s: unlocking'), instance_id, context=context)
self.db.instance_update(context, instance_id, {'locked': False})
- @exception.wrap_exception
+ @exception.wrap_exception(notifier=notifier, publisher_id=publisher_id())
def get_lock(self, context, instance_id):
"""Return the boolean state of the given instance's lock."""
context = context.elevated()
@@ -967,7 +983,7 @@ class ComputeManager(manager.SchedulerDependentManager):
self.driver.inject_network_info(instance, network_info)
- @exception.wrap_exception
+ @exception.wrap_exception(notifier=notifier, publisher_id=publisher_id())
def get_console_output(self, context, instance_id):
"""Send the console output for the given instance."""
context = context.elevated()
@@ -977,7 +993,7 @@ class ComputeManager(manager.SchedulerDependentManager):
output = self.driver.get_console_output(instance_ref)
return output.decode('utf-8', 'replace').encode('ascii', 'replace')
- @exception.wrap_exception
+ @exception.wrap_exception(notifier=notifier, publisher_id=publisher_id())
def get_ajax_console(self, context, instance_id):
"""Return connection information for an ajax console."""
context = context.elevated()
@@ -985,7 +1001,7 @@ class ComputeManager(manager.SchedulerDependentManager):
instance_ref = self.db.instance_get(context, instance_id)
return self.driver.get_ajax_console(instance_ref)
- @exception.wrap_exception
+ @exception.wrap_exception(notifier=notifier, publisher_id=publisher_id())
def get_vnc_console(self, context, instance_id):
"""Return connection information for a vnc console."""
context = context.elevated()
@@ -1048,7 +1064,7 @@ class ComputeManager(manager.SchedulerDependentManager):
return True
- @exception.wrap_exception
+ @exception.wrap_exception(notifier=notifier, publisher_id=publisher_id())
@checks_instance_lock
def _detach_volume(self, context, instance_id, volume_id, destroy_bdm):
"""Detach a volume from an instance."""
@@ -1083,7 +1099,7 @@ class ComputeManager(manager.SchedulerDependentManager):
"""
self.volume_manager.remove_compute_volume(context, volume_id)
- @exception.wrap_exception
+ @exception.wrap_exception(notifier=notifier, publisher_id=publisher_id())
def compare_cpu(self, context, cpu_info):
"""Checks that the host cpu is compatible with a cpu given by xml.
@@ -1094,7 +1110,7 @@ class ComputeManager(manager.SchedulerDependentManager):
"""
return self.driver.compare_cpu(cpu_info)
- @exception.wrap_exception
+ @exception.wrap_exception(notifier=notifier, publisher_id=publisher_id())
def create_shared_storage_test_file(self, context):
"""Makes tmpfile under FLAGS.instance_path.
@@ -1114,7 +1130,7 @@ class ComputeManager(manager.SchedulerDependentManager):
os.close(fd)
return os.path.basename(tmp_file)
- @exception.wrap_exception
+ @exception.wrap_exception(notifier=notifier, publisher_id=publisher_id())
def check_shared_storage_test_file(self, context, filename):
"""Confirms existence of the tmpfile under FLAGS.instances_path.
@@ -1126,7 +1142,7 @@ class ComputeManager(manager.SchedulerDependentManager):
if not os.path.exists(tmp_file):
raise exception.FileNotFound(file_path=tmp_file)
- @exception.wrap_exception
+ @exception.wrap_exception(notifier=notifier, publisher_id=publisher_id())
def cleanup_shared_storage_test_file(self, context, filename):
"""Removes existence of the tmpfile under FLAGS.instances_path.
@@ -1137,7 +1153,7 @@ class ComputeManager(manager.SchedulerDependentManager):
tmp_file = os.path.join(FLAGS.instances_path, filename)
os.remove(tmp_file)
- @exception.wrap_exception
+ @exception.wrap_exception(notifier=notifier, publisher_id=publisher_id())
def update_available_resource(self, context):
"""See comments update_resource_info.
diff --git a/nova/console/manager.py b/nova/console/manager.py
index e0db21666..2c823b763 100644
--- a/nova/console/manager.py
+++ b/nova/console/manager.py
@@ -56,7 +56,7 @@ class ConsoleProxyManager(manager.Manager):
def init_host(self):
self.driver.init_host()
- @exception.wrap_exception
+ @exception.wrap_exception()
def add_console(self, context, instance_id, password=None,
port=None, **kwargs):
instance = self.db.instance_get(context, instance_id)
@@ -83,7 +83,7 @@ class ConsoleProxyManager(manager.Manager):
self.driver.setup_console(context, console)
return console['id']
- @exception.wrap_exception
+ @exception.wrap_exception()
def remove_console(self, context, console_id, **_kwargs):
try:
console = self.db.console_get(context, console_id)
diff --git a/nova/console/vmrc_manager.py b/nova/console/vmrc_manager.py
index acecc1075..0b5ce4a49 100644
--- a/nova/console/vmrc_manager.py
+++ b/nova/console/vmrc_manager.py
@@ -77,7 +77,7 @@ class ConsoleVMRCManager(manager.Manager):
self.driver.setup_console(context, console)
return console
- @exception.wrap_exception
+ @exception.wrap_exception()
def add_console(self, context, instance_id, password=None,
port=None, **kwargs):
"""Adds a console for the instance.
@@ -107,7 +107,7 @@ class ConsoleVMRCManager(manager.Manager):
instance)
return console['id']
- @exception.wrap_exception
+ @exception.wrap_exception()
def remove_console(self, context, console_id, **_kwargs):
"""Removes a console entry."""
try:
diff --git a/nova/db/api.py b/nova/db/api.py
index c9d5bc72b..2efbf957d 100644
--- a/nova/db/api.py
+++ b/nova/db/api.py
@@ -995,10 +995,16 @@ def block_device_mapping_create(context, values):
def block_device_mapping_update(context, bdm_id, values):
- """Create an entry of block device mapping"""
+ """Update an entry of block device mapping"""
return IMPL.block_device_mapping_update(context, bdm_id, values)
+def block_device_mapping_update_or_create(context, values):
+ """Update an entry of block device mapping.
+ If not existed, create a new entry"""
+ return IMPL.block_device_mapping_update_or_create(context, values)
+
+
def block_device_mapping_get_all_by_instance(context, instance_id):
"""Get all block device mapping belonging to a instance"""
return IMPL.block_device_mapping_get_all_by_instance(context, instance_id)
diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py
index a90b61e39..33bdd767a 100644
--- a/nova/db/sqlalchemy/api.py
+++ b/nova/db/sqlalchemy/api.py
@@ -116,8 +116,23 @@ def require_context(f):
return wrapper
+def require_instance_exists(f):
+ """Decorator to require the specified instance to exist.
+
+ Requres the wrapped function to use context and instance_id as
+ their first two arguments.
+ """
+
+ def wrapper(context, instance_id, *args, **kwargs):
+ db.api.instance_get(context, instance_id)
+ return f(context, instance_id, *args, **kwargs)
+ wrapper.__name__ = f.__name__
+ return wrapper
+
+
###################
+
@require_admin_context
def service_destroy(context, service_id):
session = get_session()
@@ -937,6 +952,7 @@ def virtual_interface_get_by_fixed_ip(context, fixed_ip_id):
@require_context
+@require_instance_exists
def virtual_interface_get_by_instance(context, instance_id):
"""Gets all virtual interfaces for instance.
@@ -2229,6 +2245,23 @@ def block_device_mapping_update(context, bdm_id, values):
@require_context
+def block_device_mapping_update_or_create(context, values):
+ session = get_session()
+ with session.begin():
+ result = session.query(models.BlockDeviceMapping).\
+ filter_by(instance_id=values['instance_id']).\
+ filter_by(device_name=values['device_name']).\
+ filter_by(deleted=False).\
+ first()
+ if not result:
+ bdm_ref = models.BlockDeviceMapping()
+ bdm_ref.update(values)
+ bdm_ref.save(session=session)
+ else:
+ result.update(values)
+
+
+@require_context
def block_device_mapping_get_all_by_instance(context, instance_id):
session = get_session()
result = session.query(models.BlockDeviceMapping).\
@@ -3092,14 +3125,6 @@ def zone_get_all(context):
####################
-def require_instance_exists(func):
- def new_func(context, instance_id, *args, **kwargs):
- db.api.instance_get(context, instance_id)
- return func(context, instance_id, *args, **kwargs)
- new_func.__name__ = func.__name__
- return new_func
-
-
@require_context
@require_instance_exists
def instance_metadata_get(context, instance_id):
diff --git a/nova/db/sqlalchemy/migrate_repo/versions/032_add_root_device_name.py b/nova/db/sqlalchemy/migrate_repo/versions/032_add_root_device_name.py
new file mode 100644
index 000000000..6b98b9890
--- /dev/null
+++ b/nova/db/sqlalchemy/migrate_repo/versions/032_add_root_device_name.py
@@ -0,0 +1,47 @@
+# Copyright 2011 OpenStack LLC.
+# Copyright 2011 Isaku Yamahata
+#
+# 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 Column, Integer, MetaData, Table, String
+
+meta = MetaData()
+
+
+# Just for the ForeignKey and column creation to succeed, these are not the
+# actual definitions of instances or services.
+instances = Table('instances', meta,
+ Column('id', Integer(), primary_key=True, nullable=False),
+ )
+
+#
+# New Column
+#
+root_device_name = Column(
+ 'root_device_name',
+ String(length=255, convert_unicode=False, assert_unicode=None,
+ unicode_error=None, _warn_on_bytestring=False),
+ nullable=True)
+
+
+def upgrade(migrate_engine):
+ # Upgrade operations go here. Don't create your own engine;
+ # bind migrate_engine to your metadata
+ meta.bind = migrate_engine
+ instances.create_column(root_device_name)
+
+
+def downgrade(migrate_engine):
+ # Operations to reverse the above upgrade go here.
+ meta.bind = migrate_engine
+ instances.drop_column('root_device_name')
diff --git a/nova/db/sqlalchemy/models.py b/nova/db/sqlalchemy/models.py
index 639941dc8..e42f605c4 100644
--- a/nova/db/sqlalchemy/models.py
+++ b/nova/db/sqlalchemy/models.py
@@ -236,6 +236,8 @@ class Instance(BASE, NovaBase):
vm_mode = Column(String(255))
uuid = Column(String(36))
+ root_device_name = Column(String(255))
+
# TODO(vish): see Ewan's email about state improvements, probably
# should be in a driver base class or some such
# vmstate_state = running, halted, suspended, paused
diff --git a/nova/exception.py b/nova/exception.py
index 29a209c3e..cb015e694 100644
--- a/nova/exception.py
+++ b/nova/exception.py
@@ -24,8 +24,9 @@ SHOULD include dedicated exception logging.
"""
-from nova import log as logging
+from functools import wraps
+from nova import log as logging
LOG = logging.getLogger('nova.exception')
@@ -81,19 +82,49 @@ def wrap_db_error(f):
_wrap.func_name = f.func_name
-def wrap_exception(f):
- def _wrap(*args, **kw):
- try:
- return f(*args, **kw)
- except Exception, e:
- if not isinstance(e, Error):
- #exc_type, exc_value, exc_traceback = sys.exc_info()
- LOG.exception(_('Uncaught exception'))
- #logging.error(traceback.extract_stack(exc_traceback))
- raise Error(str(e))
- raise
- _wrap.func_name = f.func_name
- return _wrap
+def wrap_exception(notifier=None, publisher_id=None, event_type=None,
+ level=None):
+ """This decorator wraps a method to catch any exceptions that may
+ get thrown. It logs the exception as well as optionally sending
+ it to the notification system.
+ """
+ # TODO(sandy): Find a way to import nova.notifier.api so we don't have
+ # to pass it in as a parameter. Otherwise we get a cyclic import of
+ # nova.notifier.api -> nova.utils -> nova.exception :(
+ def inner(f):
+ def wrapped(*args, **kw):
+ try:
+ return f(*args, **kw)
+ except Exception, e:
+ if notifier:
+ payload = dict(args=args, exception=e)
+ payload.update(kw)
+
+ # Use a temp vars so we don't shadow
+ # our outer definitions.
+ temp_level = level
+ if not temp_level:
+ temp_level = notifier.ERROR
+
+ temp_type = event_type
+ if not temp_type:
+ # If f has multiple decorators, they must use
+ # functools.wraps to ensure the name is
+ # propagated.
+ temp_type = f.__name__
+
+ notifier.notify(publisher_id, temp_type, temp_level,
+ payload)
+
+ if not isinstance(e, Error):
+ #exc_type, exc_value, exc_traceback = sys.exc_info()
+ LOG.exception(_('Uncaught exception'))
+ #logging.error(traceback.extract_stack(exc_traceback))
+ raise Error(str(e))
+ raise
+
+ return wraps(f)(wrapped)
+ return inner
class NovaException(Exception):
@@ -382,6 +413,10 @@ class FixedIpNotFoundForNetworkHost(FixedIpNotFound):
"in network %(network_id)s.")
+class FixedIpNotFoundForSpecificInstance(FixedIpNotFound):
+ message = _("Instance %(instance_id)s doesn't have fixed ip '%(ip)s'.")
+
+
class FixedIpNotFoundForVirtualInterface(FixedIpNotFound):
message = _("Virtual interface %(vif_id)s has zero associated fixed ips.")
diff --git a/nova/image/fake.py b/nova/image/fake.py
index c4b3d5fd6..28e912534 100644
--- a/nova/image/fake.py
+++ b/nova/image/fake.py
@@ -137,7 +137,11 @@ class _FakeImageService(service.BaseImageService):
try:
image_id = metadata['id']
except KeyError:
- image_id = random.randint(0, 2 ** 31 - 1)
+ while True:
+ image_id = random.randint(0, 2 ** 31 - 1)
+ if not self.images.get(str(image_id)):
+ break
+
image_id = str(image_id)
if self.images.get(image_id):
@@ -176,3 +180,8 @@ _fakeImageService = _FakeImageService()
def FakeImageService():
return _fakeImageService
+
+
+def FakeImageService_reset():
+ global _fakeImageService
+ _fakeImageService = _FakeImageService()
diff --git a/nova/image/s3.py b/nova/image/s3.py
index 9e95bd698..4a3df98ba 100644
--- a/nova/image/s3.py
+++ b/nova/image/s3.py
@@ -102,18 +102,7 @@ class S3ImageService(service.BaseImageService):
key.get_contents_to_filename(local_filename)
return local_filename
- def _s3_create(self, context, metadata):
- """Gets a manifext from s3 and makes an image."""
-
- image_path = tempfile.mkdtemp(dir=FLAGS.image_decryption_dir)
-
- image_location = metadata['properties']['image_location']
- bucket_name = image_location.split('/')[0]
- manifest_path = image_location[len(bucket_name) + 1:]
- bucket = self._conn(context).get_bucket(bucket_name)
- key = bucket.get_key(manifest_path)
- manifest = key.get_contents_as_string()
-
+ def _s3_parse_manifest(self, context, metadata, manifest):
manifest = ElementTree.fromstring(manifest)
image_format = 'ami'
image_type = 'machine'
@@ -141,6 +130,28 @@ class S3ImageService(service.BaseImageService):
except Exception:
arch = 'x86_64'
+ # NOTE(yamahata):
+ # EC2 ec2-budlne-image --block-device-mapping accepts
+ # <virtual name>=<device name> where
+ # virtual name = {ami, root, swap, ephemeral<N>}
+ # where N is no negative integer
+ # device name = the device name seen by guest kernel.
+ # They are converted into
+ # block_device_mapping/mapping/{virtual, device}
+ #
+ # Do NOT confuse this with ec2-register's block device mapping
+ # argument.
+ mappings = []
+ try:
+ block_device_mapping = manifest.findall('machine_configuration/'
+ 'block_device_mapping/'
+ 'mapping')
+ for bdm in block_device_mapping:
+ mappings.append({'virtual': bdm.find('virtual').text,
+ 'device': bdm.find('device').text})
+ except Exception:
+ mappings = []
+
properties = metadata['properties']
properties['project_id'] = context.project_id
properties['architecture'] = arch
@@ -151,6 +162,9 @@ class S3ImageService(service.BaseImageService):
if ramdisk_id:
properties['ramdisk_id'] = ec2utils.ec2_id_to_id(ramdisk_id)
+ if mappings:
+ properties['mappings'] = mappings
+
metadata.update({'disk_format': image_format,
'container_format': image_format,
'status': 'queued',
@@ -158,6 +172,21 @@ class S3ImageService(service.BaseImageService):
'properties': properties})
metadata['properties']['image_state'] = 'pending'
image = self.service.create(context, metadata)
+ return manifest, image
+
+ def _s3_create(self, context, metadata):
+ """Gets a manifext from s3 and makes an image."""
+
+ image_path = tempfile.mkdtemp(dir=FLAGS.image_decryption_dir)
+
+ image_location = metadata['properties']['image_location']
+ bucket_name = image_location.split('/')[0]
+ manifest_path = image_location[len(bucket_name) + 1:]
+ bucket = self._conn(context).get_bucket(bucket_name)
+ key = bucket.get_key(manifest_path)
+ manifest = key.get_contents_as_string()
+
+ manifest, image = self._s3_parse_manifest(context, metadata, manifest)
image_id = image['id']
def delayed_create():
diff --git a/nova/network/api.py b/nova/network/api.py
index 38560b037..4f810f427 100644
--- a/nova/network/api.py
+++ b/nova/network/api.py
@@ -165,6 +165,14 @@ class API(base.Base):
{'method': 'add_fixed_ip_to_instance',
'args': args})
+ def remove_fixed_ip_from_instance(self, context, instance_id, address):
+ """Removes a fixed ip from instance from specified network."""
+ args = {'instance_id': instance_id,
+ 'address': address}
+ rpc.cast(context, FLAGS.network_topic,
+ {'method': 'remove_fixed_ip_from_instance',
+ 'args': args})
+
def add_network_to_project(self, context, project_id):
"""Force adds another network to a project."""
rpc.cast(context, FLAGS.network_topic,
diff --git a/nova/network/manager.py b/nova/network/manager.py
index 4568d0fa7..a411ad9ff 100644
--- a/nova/network/manager.py
+++ b/nova/network/manager.py
@@ -123,10 +123,12 @@ class RPCAllocateFixedIP(object):
used since they share code to RPC.call allocate_fixed_ip on the
correct network host to configure dnsmasq
"""
- def _allocate_fixed_ips(self, context, instance_id, host, networks):
+ def _allocate_fixed_ips(self, context, instance_id, host, networks,
+ **kwargs):
"""Calls allocate_fixed_ip once for each network."""
green_pool = greenpool.GreenPool()
+ vpn = kwargs.pop('vpn')
for network in networks:
# NOTE(vish): if we are not multi_host pass to the network host
if not network['multi_host']:
@@ -142,13 +144,14 @@ class RPCAllocateFixedIP(object):
args = {}
args['instance_id'] = instance_id
args['network_id'] = network['id']
+ args['vpn'] = vpn
green_pool.spawn_n(rpc.call, context, topic,
{'method': '_rpc_allocate_fixed_ip',
'args': args})
else:
# i am the correct host, run here
- self.allocate_fixed_ip(context, instance_id, network)
+ self.allocate_fixed_ip(context, instance_id, network, vpn=vpn)
# wait for all of the allocates (if any) to finish
green_pool.waitall()
@@ -361,8 +364,15 @@ class NetworkManager(manager.SchedulerDependentManager):
# TODO(tr3buchet) maybe this needs to be updated in the future if
# there is a better way to determine which networks
# a non-vlan instance should connect to
- return [network for network in self.db.network_get_all(context)
- if not network['vlan']]
+ try:
+ networks = self.db.network_get_all(context)
+ except exception.NoNetworksFound:
+ # we don't care if no networks are found
+ pass
+
+ # return only networks which are not vlan networks
+ return [network for network in networks if
+ not network['vlan']]
def allocate_for_instance(self, context, **kwargs):
"""Handles allocating the various network resources for an instance.
@@ -373,6 +383,7 @@ class NetworkManager(manager.SchedulerDependentManager):
host = kwargs.pop('host')
project_id = kwargs.pop('project_id')
type_id = kwargs.pop('instance_type_id')
+ vpn = kwargs.pop('vpn')
admin_context = context.elevated()
LOG.debug(_("network allocations for instance %s"), instance_id,
context=context)
@@ -380,7 +391,8 @@ class NetworkManager(manager.SchedulerDependentManager):
project_id)
LOG.warn(networks)
self._allocate_mac_addresses(context, instance_id, networks)
- self._allocate_fixed_ips(admin_context, instance_id, host, networks)
+ self._allocate_fixed_ips(admin_context, instance_id, host, networks,
+ vpn=vpn)
return self.get_instance_nw_info(context, instance_id, type_id, host)
def deallocate_for_instance(self, context, **kwargs):
@@ -499,6 +511,16 @@ class NetworkManager(manager.SchedulerDependentManager):
networks = [self.db.network_get(context, network_id)]
self._allocate_fixed_ips(context, instance_id, host, networks)
+ def remove_fixed_ip_from_instance(self, context, instance_id, address):
+ """Removes a fixed ip from an instance from specified network."""
+ fixed_ips = self.db.fixed_ip_get_by_instance(context, instance_id)
+ for fixed_ip in fixed_ips:
+ if fixed_ip['address'] == address:
+ self.deallocate_fixed_ip(context, address)
+ return
+ raise exception.FixedIpNotFoundForSpecificInstance(
+ instance_id=instance_id, ip=address)
+
def allocate_fixed_ip(self, context, instance_id, network, **kwargs):
"""Gets a fixed ip from the pool."""
# TODO(vish): when this is called by compute, we can associate compute
@@ -658,7 +680,8 @@ class NetworkManager(manager.SchedulerDependentManager):
'address': address,
'reserved': reserved})
- def _allocate_fixed_ips(self, context, instance_id, host, networks):
+ def _allocate_fixed_ips(self, context, instance_id, host, networks,
+ **kwargs):
"""Calls allocate_fixed_ip once for each network."""
raise NotImplementedError()
@@ -705,7 +728,8 @@ class FlatManager(NetworkManager):
timeout_fixed_ips = False
- def _allocate_fixed_ips(self, context, instance_id, host, networks):
+ def _allocate_fixed_ips(self, context, instance_id, host, networks,
+ **kwargs):
"""Calls allocate_fixed_ip once for each network."""
for network in networks:
self.allocate_fixed_ip(context, instance_id, network)
@@ -713,7 +737,7 @@ class FlatManager(NetworkManager):
def deallocate_fixed_ip(self, context, address, **kwargs):
"""Returns a fixed ip to the pool."""
super(FlatManager, self).deallocate_fixed_ip(context, address,
- **kwargs)
+ **kwargs)
self.db.fixed_ip_disassociate(context, address)
def setup_compute_network(self, context, instance_id):
@@ -752,6 +776,14 @@ class FlatDHCPManager(FloatingIP, RPCAllocateFixedIP, NetworkManager):
self.driver.metadata_forward()
+ def allocate_fixed_ip(self, context, instance_id, network, **kwargs):
+ """Allocate flat_network fixed_ip, then setup dhcp for this network."""
+ address = super(FlatDHCPManager, self).allocate_fixed_ip(context,
+ instance_id,
+ network)
+ if not FLAGS.fake_network:
+ self.driver.update_dhcp(context, network['id'])
+
def setup_compute_network(self, context, instance_id):
"""Sets up matching networks for compute hosts.
@@ -817,6 +849,7 @@ class VlanManager(RPCAllocateFixedIP, FloatingIP, NetworkManager):
address = self.db.fixed_ip_associate_pool(context,
network['id'],
instance_id)
+
vif = self.db.virtual_interface_get_by_instance_and_network(context,
instance_id,
network['id'])
diff --git a/nova/notifier/api.py b/nova/notifier/api.py
index d49517c8b..98969fd3e 100644
--- a/nova/notifier/api.py
+++ b/nova/notifier/api.py
@@ -17,7 +17,9 @@ import uuid
from nova import flags
from nova import utils
+from nova import log as logging
+LOG = logging.getLogger('nova.exception')
FLAGS = flags.FLAGS
@@ -37,6 +39,12 @@ class BadPriorityException(Exception):
pass
+def publisher_id(service, host=None):
+ if not host:
+ host = FLAGS.host
+ return "%s.%s" % (service, host)
+
+
def notify(publisher_id, event_type, priority, payload):
"""
Sends a notification using the specified driver
@@ -79,4 +87,8 @@ def notify(publisher_id, event_type, priority, payload):
priority=priority,
payload=payload,
timestamp=str(utils.utcnow()))
- driver.notify(msg)
+ try:
+ driver.notify(msg)
+ except Exception, e:
+ LOG.exception(_("Problem '%(e)s' attempting to "
+ "send to notification system." % locals()))
diff --git a/nova/rpc.py b/nova/rpc.py
index f52f377b0..e2771ca88 100644
--- a/nova/rpc.py
+++ b/nova/rpc.py
@@ -219,7 +219,7 @@ class AdapterConsumer(Consumer):
return
self.pool.spawn_n(self._process_data, msg_id, ctxt, method, args)
- @exception.wrap_exception
+ @exception.wrap_exception()
def _process_data(self, msg_id, ctxt, method, args):
"""Thread that maigcally looks for a method on the proxy
object and calls it.
diff --git a/nova/scheduler/driver.py b/nova/scheduler/driver.py
index d4a30255d..1bfa7740a 100644
--- a/nova/scheduler/driver.py
+++ b/nova/scheduler/driver.py
@@ -31,6 +31,7 @@ from nova import rpc
from nova import utils
from nova.compute import power_state
+
FLAGS = flags.FLAGS
flags.DEFINE_integer('service_down_time', 60,
'maximum time since last checkin for up service')
diff --git a/nova/scheduler/zone_aware_scheduler.py b/nova/scheduler/zone_aware_scheduler.py
index 1cc98e48b..c429fdfcc 100644
--- a/nova/scheduler/zone_aware_scheduler.py
+++ b/nova/scheduler/zone_aware_scheduler.py
@@ -178,12 +178,14 @@ class ZoneAwareScheduler(driver.Scheduler):
to adjust the weights returned from the child zones. Alters
child_results in place.
"""
- for zone, result in child_results:
+ for zone_id, result in child_results:
if not result:
continue
+ assert isinstance(zone_id, int)
+
for zone_rec in zones:
- if zone_rec['api_url'] != zone:
+ if zone_rec['id'] != zone_id:
continue
for item in result:
@@ -196,7 +198,7 @@ class ZoneAwareScheduler(driver.Scheduler):
item['raw_weight'] = raw_weight
except KeyError:
LOG.exception(_("Bad child zone scaling values "
- "for Zone: %(zone)s") % locals())
+ "for Zone: %(zone_id)s") % locals())
def schedule_run_instance(self, context, instance_id, request_spec,
*args, **kwargs):
diff --git a/nova/scheduler/zone_manager.py b/nova/scheduler/zone_manager.py
index 6093443a9..efdac06e1 100644
--- a/nova/scheduler/zone_manager.py
+++ b/nova/scheduler/zone_manager.py
@@ -137,17 +137,30 @@ class ZoneManager(object):
# But it's likely to change once we understand what the Best-Match
# code will need better.
combined = {} # { <service>_<cap> : (min, max), ... }
+ stale_host_services = {} # { host1 : [svc1, svc2], host2 :[svc1]}
for host, host_dict in hosts_dict.iteritems():
for service_name, service_dict in host_dict.iteritems():
if not service_dict.get("enabled", True):
# Service is disabled; do no include it
continue
+
+ #Check if the service capabilities became stale
+ if self.host_service_caps_stale(host, service_name):
+ if host not in stale_host_services:
+ stale_host_services[host] = [] # Adding host key once
+ stale_host_services[host].append(service_name)
+ continue
for cap, value in service_dict.iteritems():
+ if cap == "timestamp": # Timestamp is not needed
+ continue
key = "%s_%s" % (service_name, cap)
min_value, max_value = combined.get(key, (value, value))
min_value = min(min_value, value)
max_value = max(max_value, value)
combined[key] = (min_value, max_value)
+
+ # Delete the expired host services
+ self.delete_expired_host_services(stale_host_services)
return combined
def _refresh_from_db(self, context):
@@ -186,5 +199,24 @@ class ZoneManager(object):
logging.debug(_("Received %(service_name)s service update from "
"%(host)s: %(capabilities)s") % locals())
service_caps = self.service_states.get(host, {})
+ capabilities["timestamp"] = utils.utcnow() # Reported time
service_caps[service_name] = capabilities
self.service_states[host] = service_caps
+
+ def host_service_caps_stale(self, host, service):
+ """Check if host service capabilites are not recent enough."""
+ allowed_time_diff = FLAGS.periodic_interval * 3
+ caps = self.service_states[host][service]
+ if (utils.utcnow() - caps["timestamp"]) <= \
+ datetime.timedelta(seconds=allowed_time_diff):
+ return False
+ return True
+
+ def delete_expired_host_services(self, host_services_dict):
+ """Delete all the inactive host services information."""
+ for host, services in host_services_dict.iteritems():
+ service_caps = self.service_states[host]
+ for service in services:
+ del service_caps[service]
+ if len(service_caps) == 0: # Delete host if no services
+ del self.service_states[host]
diff --git a/nova/test.py b/nova/test.py
index 6fb6b5a82..9790b0aa1 100644
--- a/nova/test.py
+++ b/nova/test.py
@@ -31,6 +31,7 @@ import unittest
import mox
import nose.plugins.skip
+import nova.image.fake
import shutil
import stubout
from eventlet import greenthread
@@ -119,6 +120,9 @@ class TestCase(unittest.TestCase):
if hasattr(fake.FakeConnection, '_instance'):
del fake.FakeConnection._instance
+ if FLAGS.image_service == 'nova.image.fake.FakeImageService':
+ nova.image.fake.FakeImageService_reset()
+
# Reset any overriden flags
self.reset_flags()
@@ -248,3 +252,15 @@ class TestCase(unittest.TestCase):
for d1, d2 in zip(L1, L2):
self.assertDictMatch(d1, d2, approx_equal=approx_equal,
tolerance=tolerance)
+
+ def assertSubDictMatch(self, sub_dict, super_dict):
+ """Assert a sub_dict is subset of super_dict."""
+ self.assertTrue(set(sub_dict.keys()).issubset(set(super_dict.keys())))
+ for k, sub_value in sub_dict.items():
+ super_value = super_dict[k]
+ if isinstance(sub_value, dict):
+ self.assertSubDictMatch(sub_value, super_value)
+ elif 'DONTCARE' in (sub_value, super_value):
+ continue
+ else:
+ self.assertEqual(sub_value, super_value)
diff --git a/nova/tests/api/openstack/contrib/test_floating_ips.py b/nova/tests/api/openstack/contrib/test_floating_ips.py
index de1eb2f53..de006d088 100644
--- a/nova/tests/api/openstack/contrib/test_floating_ips.py
+++ b/nova/tests/api/openstack/contrib/test_floating_ips.py
@@ -139,7 +139,9 @@ class FloatingIpTest(test.TestCase):
def test_floating_ip_allocate(self):
req = webob.Request.blank('/v1.1/os-floating-ips')
req.method = 'POST'
+ req.headers['Content-Type'] = 'application/json'
res = req.get_response(fakes.wsgi_app())
+ print res
self.assertEqual(res.status_int, 200)
ip = json.loads(res.body)['allocated']
expected = {
@@ -177,6 +179,7 @@ class FloatingIpTest(test.TestCase):
def test_floating_ip_disassociate(self):
req = webob.Request.blank('/v1.1/os-floating-ips/1/disassociate')
req.method = 'POST'
+ req.headers['Content-Type'] = 'application/json'
res = req.get_response(fakes.wsgi_app())
self.assertEqual(res.status_int, 200)
ip = json.loads(res.body)['disassociated']
diff --git a/nova/tests/api/openstack/contrib/test_multinic_xs.py b/nova/tests/api/openstack/contrib/test_multinic_xs.py
new file mode 100644
index 000000000..b0a9f7676
--- /dev/null
+++ b/nova/tests/api/openstack/contrib/test_multinic_xs.py
@@ -0,0 +1,115 @@
+# 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 json
+import stubout
+import webob
+
+from nova import compute
+from nova import context
+from nova import test
+from nova.tests.api.openstack import fakes
+
+
+last_add_fixed_ip = (None, None)
+last_remove_fixed_ip = (None, None)
+
+
+def compute_api_add_fixed_ip(self, context, instance_id, network_id):
+ global last_add_fixed_ip
+
+ last_add_fixed_ip = (instance_id, network_id)
+
+
+def compute_api_remove_fixed_ip(self, context, instance_id, address):
+ global last_remove_fixed_ip
+
+ last_remove_fixed_ip = (instance_id, address)
+
+
+class FixedIpTest(test.TestCase):
+ def setUp(self):
+ super(FixedIpTest, self).setUp()
+ self.stubs = stubout.StubOutForTesting()
+ fakes.FakeAuthManager.reset_fake_data()
+ fakes.FakeAuthDatabase.data = {}
+ fakes.stub_out_networking(self.stubs)
+ fakes.stub_out_rate_limiting(self.stubs)
+ fakes.stub_out_auth(self.stubs)
+ self.stubs.Set(compute.api.API, "add_fixed_ip",
+ compute_api_add_fixed_ip)
+ self.stubs.Set(compute.api.API, "remove_fixed_ip",
+ compute_api_remove_fixed_ip)
+ self.context = context.get_admin_context()
+
+ def tearDown(self):
+ self.stubs.UnsetAll()
+ super(FixedIpTest, self).tearDown()
+
+ def test_add_fixed_ip(self):
+ global last_add_fixed_ip
+ last_add_fixed_ip = (None, None)
+
+ body = dict(addFixedIp=dict(networkId='test_net'))
+ req = webob.Request.blank('/v1.1/servers/test_inst/action')
+ req.method = 'POST'
+ req.body = json.dumps(body)
+ req.headers['content-type'] = 'application/json'
+
+ resp = req.get_response(fakes.wsgi_app())
+ self.assertEqual(resp.status_int, 202)
+ self.assertEqual(last_add_fixed_ip, ('test_inst', 'test_net'))
+
+ def test_add_fixed_ip_no_network(self):
+ global last_add_fixed_ip
+ last_add_fixed_ip = (None, None)
+
+ body = dict(addFixedIp=dict())
+ req = webob.Request.blank('/v1.1/servers/test_inst/action')
+ req.method = 'POST'
+ req.body = json.dumps(body)
+ req.headers['content-type'] = 'application/json'
+
+ resp = req.get_response(fakes.wsgi_app())
+ self.assertEqual(resp.status_int, 422)
+ self.assertEqual(last_add_fixed_ip, (None, None))
+
+ def test_remove_fixed_ip(self):
+ global last_remove_fixed_ip
+ last_remove_fixed_ip = (None, None)
+
+ body = dict(removeFixedIp=dict(address='10.10.10.1'))
+ req = webob.Request.blank('/v1.1/servers/test_inst/action')
+ req.method = 'POST'
+ req.body = json.dumps(body)
+ req.headers['content-type'] = 'application/json'
+
+ resp = req.get_response(fakes.wsgi_app())
+ self.assertEqual(resp.status_int, 202)
+ self.assertEqual(last_remove_fixed_ip, ('test_inst', '10.10.10.1'))
+
+ def test_remove_fixed_ip_no_address(self):
+ global last_remove_fixed_ip
+ last_remove_fixed_ip = (None, None)
+
+ body = dict(removeFixedIp=dict())
+ req = webob.Request.blank('/v1.1/servers/test_inst/action')
+ req.method = 'POST'
+ req.body = json.dumps(body)
+ req.headers['content-type'] = 'application/json'
+
+ resp = req.get_response(fakes.wsgi_app())
+ self.assertEqual(resp.status_int, 422)
+ self.assertEqual(last_remove_fixed_ip, (None, None))
diff --git a/nova/tests/api/openstack/test_common.py b/nova/tests/api/openstack/test_common.py
index 29cb8b944..4c4d03995 100644
--- a/nova/tests/api/openstack/test_common.py
+++ b/nova/tests/api/openstack/test_common.py
@@ -190,3 +190,60 @@ class PaginationParamsTest(test.TestCase):
req = Request.blank('/?limit=20&marker=40')
self.assertEqual(common.get_pagination_params(req),
{'marker': 40, 'limit': 20})
+
+
+class MiscFunctionsTest(test.TestCase):
+
+ def test_remove_version_from_href(self):
+ fixture = 'http://www.testsite.com/v1.1/images'
+ expected = 'http://www.testsite.com/images'
+ actual = common.remove_version_from_href(fixture)
+ self.assertEqual(actual, expected)
+
+ def test_remove_version_from_href_2(self):
+ fixture = 'http://www.testsite.com/v1.1/'
+ expected = 'http://www.testsite.com/'
+ actual = common.remove_version_from_href(fixture)
+ self.assertEqual(actual, expected)
+
+ def test_remove_version_from_href_3(self):
+ fixture = 'http://www.testsite.com/v10.10'
+ expected = 'http://www.testsite.com'
+ actual = common.remove_version_from_href(fixture)
+ self.assertEqual(actual, expected)
+
+ def test_remove_version_from_href_4(self):
+ fixture = 'http://www.testsite.com/v1.1/images/v10.5'
+ expected = 'http://www.testsite.com/images/v10.5'
+ actual = common.remove_version_from_href(fixture)
+ self.assertEqual(actual, expected)
+
+ def test_remove_version_from_href_bad_request(self):
+ fixture = 'http://www.testsite.com/1.1/images'
+ self.assertRaises(ValueError,
+ common.remove_version_from_href,
+ fixture)
+
+ def test_remove_version_from_href_bad_request_2(self):
+ fixture = 'http://www.testsite.com/v/images'
+ self.assertRaises(ValueError,
+ common.remove_version_from_href,
+ fixture)
+
+ def test_remove_version_from_href_bad_request_3(self):
+ fixture = 'http://www.testsite.com/v1.1images'
+ self.assertRaises(ValueError,
+ common.remove_version_from_href,
+ fixture)
+
+ def test_get_id_from_href(self):
+ fixture = 'http://www.testsite.com/dir/45'
+ actual = common.get_id_from_href(fixture)
+ expected = 45
+ self.assertEqual(actual, expected)
+
+ def test_get_id_from_href_bad_request(self):
+ fixture = 'http://45'
+ self.assertRaises(ValueError,
+ common.get_id_from_href,
+ fixture)
diff --git a/nova/tests/api/openstack/test_images.py b/nova/tests/api/openstack/test_images.py
index 54601f35a..534460d46 100644
--- a/nova/tests/api/openstack/test_images.py
+++ b/nova/tests/api/openstack/test_images.py
@@ -401,15 +401,27 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
href = "http://localhost/v1.1/images/124"
bookmark = "http://localhost/images/124"
+ server_href = "http://localhost/v1.1/servers/42"
+ server_bookmark = "http://localhost/servers/42"
expected_image = {
"image": {
"id": 124,
"name": "queued snapshot",
- "serverRef": "http://localhost/v1.1/servers/42",
"updated": self.NOW_API_FORMAT,
"created": self.NOW_API_FORMAT,
"status": "QUEUED",
+ 'server': {
+ 'id': 42,
+ "links": [{
+ "rel": "self",
+ "href": server_href,
+ },
+ {
+ "rel": "bookmark",
+ "href": server_bookmark,
+ }],
+ },
"metadata": {
"instance_ref": "http://localhost/v1.1/servers/42",
"user_id": "1",
@@ -556,14 +568,16 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
test_image = {
"id": image["id"],
"name": image["name"],
- "links": [{
- "rel": "self",
- "href": href,
- },
- {
- "rel": "bookmark",
- "href": bookmark,
- }],
+ "links": [
+ {
+ "rel": "self",
+ "href": href,
+ },
+ {
+ "rel": "bookmark",
+ "href": bookmark,
+ },
+ ],
}
self.assertTrue(test_image in response_list)
@@ -628,6 +642,8 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
response_dict = json.loads(response.body)
response_list = response_dict["images"]
+ server_href = "http://localhost/v1.1/servers/42"
+ server_bookmark = "http://localhost/servers/42"
expected = [{
'id': 123,
@@ -652,10 +668,20 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
u'instance_ref': u'http://localhost/v1.1/servers/42',
u'user_id': u'1',
},
- 'serverRef': "http://localhost/v1.1/servers/42",
'updated': self.NOW_API_FORMAT,
'created': self.NOW_API_FORMAT,
'status': 'QUEUED',
+ 'server': {
+ 'id': 42,
+ "links": [{
+ "rel": "self",
+ "href": server_href,
+ },
+ {
+ "rel": "bookmark",
+ "href": server_bookmark,
+ }],
+ },
"links": [{
"rel": "self",
"href": "http://localhost/v1.1/images/124",
@@ -672,11 +698,21 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
u'instance_ref': u'http://localhost/v1.1/servers/42',
u'user_id': u'1',
},
- 'serverRef': "http://localhost/v1.1/servers/42",
'updated': self.NOW_API_FORMAT,
'created': self.NOW_API_FORMAT,
'status': 'SAVING',
'progress': 0,
+ 'server': {
+ 'id': 42,
+ "links": [{
+ "rel": "self",
+ "href": server_href,
+ },
+ {
+ "rel": "bookmark",
+ "href": server_bookmark,
+ }],
+ },
"links": [{
"rel": "self",
"href": "http://localhost/v1.1/images/125",
@@ -693,10 +729,20 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
u'instance_ref': u'http://localhost/v1.1/servers/42',
u'user_id': u'1',
},
- 'serverRef': "http://localhost/v1.1/servers/42",
'updated': self.NOW_API_FORMAT,
'created': self.NOW_API_FORMAT,
'status': 'ACTIVE',
+ 'server': {
+ 'id': 42,
+ "links": [{
+ "rel": "self",
+ "href": server_href,
+ },
+ {
+ "rel": "bookmark",
+ "href": server_bookmark,
+ }],
+ },
"links": [{
"rel": "self",
"href": "http://localhost/v1.1/images/126",
@@ -713,10 +759,20 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
u'instance_ref': u'http://localhost/v1.1/servers/42',
u'user_id': u'1',
},
- 'serverRef': "http://localhost/v1.1/servers/42",
'updated': self.NOW_API_FORMAT,
'created': self.NOW_API_FORMAT,
'status': 'FAILED',
+ 'server': {
+ 'id': 42,
+ "links": [{
+ "rel": "self",
+ "href": server_href,
+ },
+ {
+ "rel": "bookmark",
+ "href": server_bookmark,
+ }],
+ },
"links": [{
"rel": "self",
"href": "http://localhost/v1.1/images/127",
@@ -1036,6 +1092,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
def test_create_image_v1_1_actual_server_ref(self):
serverRef = 'http://localhost/v1.1/servers/1'
+ serverBookmark = 'http://localhost/servers/1'
body = dict(image=dict(serverRef=serverRef, name='Backup 1'))
req = webob.Request.blank('/v1.1/images')
req.method = 'POST'
@@ -1044,7 +1101,47 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
response = req.get_response(fakes.wsgi_app())
self.assertEqual(200, response.status_int)
result = json.loads(response.body)
- self.assertEqual(result['image']['serverRef'], serverRef)
+ expected = {
+ 'id': 1,
+ 'links': [
+ {
+ 'rel': 'self',
+ 'href': serverRef,
+ },
+ {
+ 'rel': 'bookmark',
+ 'href': serverBookmark,
+ },
+ ]
+ }
+ self.assertEqual(result['image']['server'], expected)
+
+ def test_create_image_v1_1_actual_server_ref_port(self):
+
+ serverRef = 'http://localhost:8774/v1.1/servers/1'
+ serverBookmark = 'http://localhost:8774/servers/1'
+ body = dict(image=dict(serverRef=serverRef, name='Backup 1'))
+ req = webob.Request.blank('/v1.1/images')
+ req.method = 'POST'
+ req.body = json.dumps(body)
+ req.headers["content-type"] = "application/json"
+ response = req.get_response(fakes.wsgi_app())
+ self.assertEqual(200, response.status_int)
+ result = json.loads(response.body)
+ expected = {
+ 'id': 1,
+ 'links': [
+ {
+ 'rel': 'self',
+ 'href': serverRef,
+ },
+ {
+ 'rel': 'bookmark',
+ 'href': serverBookmark,
+ },
+ ]
+ }
+ self.assertEqual(result['image']['server'], expected)
def test_create_image_v1_1_server_ref_bad_hostname(self):
@@ -1067,6 +1164,28 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
response = req.get_response(fakes.wsgi_app())
self.assertEqual(400, response.status_int)
+ def test_create_image_v1_1_server_ref_missing_version(self):
+
+ serverRef = 'http://localhost/servers/1'
+ body = dict(image=dict(serverRef=serverRef, name='Backup 1'))
+ req = webob.Request.blank('/v1.1/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_image_v1_1_server_ref_missing_id(self):
+
+ serverRef = 'http://localhost/v1.1/servers'
+ body = dict(image=dict(serverRef=serverRef, name='Backup 1'))
+ req = webob.Request.blank('/v1.1/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
@@ -1115,7 +1234,9 @@ class ImageXMLSerializationTest(test.TestCase):
TIMESTAMP = "2010-10-11T10:30:22Z"
SERVER_HREF = 'http://localhost/v1.1/servers/123'
+ SERVER_BOOKMARK = 'http://localhost/servers/123'
IMAGE_HREF = 'http://localhost/v1.1/images/%s'
+ IMAGE_BOOKMARK = 'http://localhost/images/%s'
def test_show(self):
serializer = images.ImageXMLSerializer()
@@ -1126,16 +1247,32 @@ class ImageXMLSerializationTest(test.TestCase):
'name': 'Image1',
'created': self.TIMESTAMP,
'updated': self.TIMESTAMP,
- 'serverRef': self.SERVER_HREF,
'status': 'ACTIVE',
+ 'progress': 80,
+ 'server': {
+ 'id': 1,
+ 'links': [
+ {
+ 'href': self.SERVER_HREF,
+ 'rel': 'self',
+ },
+ {
+ 'href': self.SERVER_BOOKMARK,
+ 'rel': 'bookmark',
+ },
+ ],
+ },
'metadata': {
'key1': 'value1',
},
'links': [
{
- 'href': self.IMAGE_HREF % (1,),
+ 'href': self.IMAGE_HREF % 1,
+ 'rel': 'self',
+ },
+ {
+ 'href': self.IMAGE_BOOKMARK % 1,
'rel': 'bookmark',
- 'type': 'application/json',
},
],
},
@@ -1145,25 +1282,30 @@ class ImageXMLSerializationTest(test.TestCase):
actual = minidom.parseString(output.replace(" ", ""))
expected_server_href = self.SERVER_HREF
- expected_href = self.IMAGE_HREF % (1, )
+ expected_server_bookmark = self.SERVER_BOOKMARK
+ expected_href = self.IMAGE_HREF % 1
+ expected_bookmark = self.IMAGE_BOOKMARK % 1
expected_now = self.TIMESTAMP
expected = minidom.parseString("""
<image id="1"
+ xmlns="http://docs.openstack.org/compute/api/v1.1"
+ xmlns:atom="http://www.w3.org/2005/Atom"
name="Image1"
- serverRef="%(expected_server_href)s"
updated="%(expected_now)s"
created="%(expected_now)s"
status="ACTIVE"
- xmlns="http://docs.openstack.org/compute/api/v1.1">
- <links>
- <link href="%(expected_href)s" rel="bookmark"
- type="application/json" />
- </links>
+ progress="80">
+ <server id="1">
+ <atom:link rel="self" href="%(expected_server_href)s"/>
+ <atom:link rel="bookmark" href="%(expected_server_bookmark)s"/>
+ </server>
<metadata>
<meta key="key1">
value1
</meta>
</metadata>
+ <atom:link href="%(expected_href)s" rel="self"/>
+ <atom:link href="%(expected_bookmark)s" rel="bookmark"/>
</image>
""".replace(" ", "") % (locals()))
@@ -1178,14 +1320,29 @@ class ImageXMLSerializationTest(test.TestCase):
'name': 'Image1',
'created': self.TIMESTAMP,
'updated': self.TIMESTAMP,
- 'serverRef': self.SERVER_HREF,
'status': 'ACTIVE',
+ 'server': {
+ 'id': 1,
+ 'links': [
+ {
+ 'href': self.SERVER_HREF,
+ 'rel': 'self',
+ },
+ {
+ 'href': self.SERVER_BOOKMARK,
+ 'rel': 'bookmark',
+ },
+ ],
+ },
'metadata': {},
'links': [
{
- 'href': self.IMAGE_HREF % (1,),
+ 'href': self.IMAGE_HREF % 1,
+ 'rel': 'self',
+ },
+ {
+ 'href': self.IMAGE_BOOKMARK % 1,
'rel': 'bookmark',
- 'type': 'application/json',
},
],
},
@@ -1195,21 +1352,24 @@ class ImageXMLSerializationTest(test.TestCase):
actual = minidom.parseString(output.replace(" ", ""))
expected_server_href = self.SERVER_HREF
- expected_href = self.IMAGE_HREF % (1, )
+ expected_server_bookmark = self.SERVER_BOOKMARK
+ expected_href = self.IMAGE_HREF % 1
+ expected_bookmark = self.IMAGE_BOOKMARK % 1
expected_now = self.TIMESTAMP
expected = minidom.parseString("""
<image id="1"
+ xmlns="http://docs.openstack.org/compute/api/v1.1"
+ xmlns:atom="http://www.w3.org/2005/Atom"
name="Image1"
- serverRef="%(expected_server_href)s"
updated="%(expected_now)s"
created="%(expected_now)s"
- status="ACTIVE"
- xmlns="http://docs.openstack.org/compute/api/v1.1">
- <links>
- <link href="%(expected_href)s" rel="bookmark"
- type="application/json" />
- </links>
- <metadata />
+ status="ACTIVE">
+ <server id="1">
+ <atom:link rel="self" href="%(expected_server_href)s"/>
+ <atom:link rel="bookmark" href="%(expected_server_bookmark)s"/>
+ </server>
+ <atom:link href="%(expected_href)s" rel="self"/>
+ <atom:link href="%(expected_bookmark)s" rel="bookmark"/>
</image>
""".replace(" ", "") % (locals()))
@@ -1224,16 +1384,30 @@ class ImageXMLSerializationTest(test.TestCase):
'name': 'Image1',
'created': self.TIMESTAMP,
'updated': self.TIMESTAMP,
- 'serverRef': self.SERVER_HREF,
'status': 'ACTIVE',
+ 'server': {
+ 'id': 1,
+ 'links': [
+ {
+ 'href': self.SERVER_HREF,
+ 'rel': 'self',
+ },
+ {
+ 'href': self.SERVER_BOOKMARK,
+ 'rel': 'bookmark',
+ },
+ ],
+ },
'links': [
{
- 'href': self.IMAGE_HREF % (1,),
+ 'href': self.IMAGE_HREF % 1,
+ 'rel': 'self',
+ },
+ {
+ 'href': self.IMAGE_BOOKMARK % 1,
'rel': 'bookmark',
- 'type': 'application/json',
},
],
-
},
}
@@ -1241,21 +1415,76 @@ class ImageXMLSerializationTest(test.TestCase):
actual = minidom.parseString(output.replace(" ", ""))
expected_server_href = self.SERVER_HREF
- expected_href = self.IMAGE_HREF % (1, )
+ expected_server_bookmark = self.SERVER_BOOKMARK
+ expected_href = self.IMAGE_HREF % 1
+ expected_bookmark = self.IMAGE_BOOKMARK % 1
expected_now = self.TIMESTAMP
expected = minidom.parseString("""
<image id="1"
+ xmlns="http://docs.openstack.org/compute/api/v1.1"
+ xmlns:atom="http://www.w3.org/2005/Atom"
name="Image1"
- serverRef="%(expected_server_href)s"
updated="%(expected_now)s"
created="%(expected_now)s"
- status="ACTIVE"
- xmlns="http://docs.openstack.org/compute/api/v1.1">
- <links>
- <link href="%(expected_href)s" rel="bookmark"
- type="application/json" />
- </links>
- <metadata />
+ status="ACTIVE">
+ <server id="1">
+ <atom:link rel="self" href="%(expected_server_href)s"/>
+ <atom:link rel="bookmark" href="%(expected_server_bookmark)s"/>
+ </server>
+ <atom:link href="%(expected_href)s" rel="self"/>
+ <atom:link href="%(expected_bookmark)s" rel="bookmark"/>
+ </image>
+ """.replace(" ", "") % (locals()))
+
+ self.assertEqual(expected.toxml(), actual.toxml())
+
+ def test_show_no_server(self):
+ serializer = images.ImageXMLSerializer()
+
+ fixture = {
+ 'image': {
+ 'id': 1,
+ 'name': 'Image1',
+ 'created': self.TIMESTAMP,
+ 'updated': self.TIMESTAMP,
+ 'status': 'ACTIVE',
+ 'metadata': {
+ 'key1': 'value1',
+ },
+ 'links': [
+ {
+ 'href': self.IMAGE_HREF % 1,
+ 'rel': 'self',
+ },
+ {
+ 'href': self.IMAGE_BOOKMARK % 1,
+ 'rel': 'bookmark',
+ },
+ ],
+ },
+ }
+
+ output = serializer.serialize(fixture, 'show')
+ actual = minidom.parseString(output.replace(" ", ""))
+
+ expected_href = self.IMAGE_HREF % 1
+ expected_bookmark = self.IMAGE_BOOKMARK % 1
+ expected_now = self.TIMESTAMP
+ expected = minidom.parseString("""
+ <image id="1"
+ xmlns="http://docs.openstack.org/compute/api/v1.1"
+ xmlns:atom="http://www.w3.org/2005/Atom"
+ name="Image1"
+ updated="%(expected_now)s"
+ created="%(expected_now)s"
+ status="ACTIVE">
+ <metadata>
+ <meta key="key1">
+ value1
+ </meta>
+ </metadata>
+ <atom:link href="%(expected_href)s" rel="self"/>
+ <atom:link href="%(expected_bookmark)s" rel="bookmark"/>
</image>
""".replace(" ", "") % (locals()))
@@ -1264,70 +1493,51 @@ class ImageXMLSerializationTest(test.TestCase):
def test_index(self):
serializer = images.ImageXMLSerializer()
- fixtures = {
+ fixture = {
'images': [
{
'id': 1,
'name': 'Image1',
- 'created': self.TIMESTAMP,
- 'updated': self.TIMESTAMP,
- 'serverRef': self.SERVER_HREF,
- 'status': 'ACTIVE',
'links': [
{
- 'href': 'http://localhost/v1.1/images/1',
- 'rel': 'bookmark',
- 'type': 'application/json',
+ 'href': self.IMAGE_HREF % 1,
+ 'rel': 'self',
},
],
},
{
'id': 2,
- 'name': 'queued image',
- 'created': self.TIMESTAMP,
- 'updated': self.TIMESTAMP,
- 'serverRef': self.SERVER_HREF,
- 'status': 'QUEUED',
+ 'name': 'Image2',
'links': [
{
- 'href': 'http://localhost/v1.1/images/2',
- 'rel': 'bookmark',
- 'type': 'application/json',
+ 'href': self.IMAGE_HREF % 2,
+ 'rel': 'self',
},
],
},
- ],
+ ]
}
- output = serializer.serialize(fixtures, 'index')
+ output = serializer.serialize(fixture, 'index')
actual = minidom.parseString(output.replace(" ", ""))
- expected_serverRef = self.SERVER_HREF
+ expected_server_href = self.SERVER_HREF
+ expected_server_bookmark = self.SERVER_BOOKMARK
+ expected_href = self.IMAGE_HREF % 1
+ expected_bookmark = self.IMAGE_BOOKMARK % 1
+ expected_href_two = self.IMAGE_HREF % 2
+ expected_bookmark_two = self.IMAGE_BOOKMARK % 2
expected_now = self.TIMESTAMP
expected = minidom.parseString("""
- <images xmlns="http://docs.openstack.org/compute/api/v1.1">
- <image id="1"
- name="Image1"
- serverRef="%(expected_serverRef)s"
- updated="%(expected_now)s"
- created="%(expected_now)s"
- status="ACTIVE">
- <links>
- <link href="http://localhost/v1.1/images/1" rel="bookmark"
- type="application/json" />
- </links>
- </image>
- <image id="2"
- name="queued image"
- serverRef="%(expected_serverRef)s"
- updated="%(expected_now)s"
- created="%(expected_now)s"
- status="QUEUED">
- <links>
- <link href="http://localhost/v1.1/images/2" rel="bookmark"
- type="application/json" />
- </links>
- </image>
+ <images
+ xmlns="http://docs.openstack.org/compute/api/v1.1"
+ xmlns:atom="http://www.w3.org/2005/Atom">
+ <image id="1" name="Image1">
+ <atom:link href="%(expected_href)s" rel="self"/>
+ </image>
+ <image id="2" name="Image2">
+ <atom:link href="%(expected_href_two)s" rel="self"/>
+ </image>
</images>
""".replace(" ", "") % (locals()))
@@ -1343,10 +1553,10 @@ class ImageXMLSerializationTest(test.TestCase):
output = serializer.serialize(fixtures, 'index')
actual = minidom.parseString(output.replace(" ", ""))
- expected_serverRef = self.SERVER_HREF
- expected_now = self.TIMESTAMP
expected = minidom.parseString("""
- <images xmlns="http://docs.openstack.org/compute/api/v1.1" />
+ <images
+ xmlns="http://docs.openstack.org/compute/api/v1.1"
+ xmlns:atom="http://www.w3.org/2005/Atom" />
""".replace(" ", "") % (locals()))
self.assertEqual(expected.toxml(), actual.toxml())
@@ -1354,84 +1564,102 @@ class ImageXMLSerializationTest(test.TestCase):
def test_detail(self):
serializer = images.ImageXMLSerializer()
- fixtures = {
+ fixture = {
'images': [
{
'id': 1,
'name': 'Image1',
'created': self.TIMESTAMP,
'updated': self.TIMESTAMP,
- 'serverRef': self.SERVER_HREF,
'status': 'ACTIVE',
- 'metadata': {
- 'key1': 'value1',
- 'key2': 'value2',
+ 'server': {
+ 'id': 1,
+ 'links': [
+ {
+ 'href': self.SERVER_HREF,
+ 'rel': 'self',
+ },
+ {
+ 'href': self.SERVER_BOOKMARK,
+ 'rel': 'bookmark',
+ },
+ ],
},
'links': [
{
- 'href': 'http://localhost/v1.1/images/1',
+ 'href': self.IMAGE_HREF % 1,
+ 'rel': 'self',
+ },
+ {
+ 'href': self.IMAGE_BOOKMARK % 1,
'rel': 'bookmark',
- 'type': 'application/json',
},
],
},
{
'id': 2,
- 'name': 'queued image',
+ 'name': 'Image2',
'created': self.TIMESTAMP,
'updated': self.TIMESTAMP,
- 'serverRef': self.SERVER_HREF,
- 'metadata': {},
- 'status': 'QUEUED',
+ 'status': 'SAVING',
+ 'progress': 80,
+ 'metadata': {
+ 'key1': 'value1',
+ },
'links': [
{
- 'href': 'http://localhost/v1.1/images/2',
+ 'href': self.IMAGE_HREF % 2,
+ 'rel': 'self',
+ },
+ {
+ 'href': self.IMAGE_BOOKMARK % 2,
'rel': 'bookmark',
- 'type': 'application/json',
},
],
},
- ],
+ ]
}
- output = serializer.serialize(fixtures, 'detail')
+ output = serializer.serialize(fixture, 'detail')
actual = minidom.parseString(output.replace(" ", ""))
- expected_serverRef = self.SERVER_HREF
+ expected_server_href = self.SERVER_HREF
+ expected_server_bookmark = self.SERVER_BOOKMARK
+ expected_href = self.IMAGE_HREF % 1
+ expected_bookmark = self.IMAGE_BOOKMARK % 1
+ expected_href_two = self.IMAGE_HREF % 2
+ expected_bookmark_two = self.IMAGE_BOOKMARK % 2
expected_now = self.TIMESTAMP
expected = minidom.parseString("""
- <images xmlns="http://docs.openstack.org/compute/api/v1.1">
- <image id="1"
- name="Image1"
- serverRef="%(expected_serverRef)s"
- updated="%(expected_now)s"
- created="%(expected_now)s"
- status="ACTIVE">
- <links>
- <link href="http://localhost/v1.1/images/1" rel="bookmark"
- type="application/json" />
- </links>
- <metadata>
- <meta key="key2">
- value2
- </meta>
- <meta key="key1">
- value1
- </meta>
- </metadata>
- </image>
- <image id="2"
- name="queued image"
- serverRef="%(expected_serverRef)s"
- updated="%(expected_now)s"
- created="%(expected_now)s"
- status="QUEUED">
- <links>
- <link href="http://localhost/v1.1/images/2" rel="bookmark"
- type="application/json" />
- </links>
- <metadata />
- </image>
+ <images
+ xmlns="http://docs.openstack.org/compute/api/v1.1"
+ xmlns:atom="http://www.w3.org/2005/Atom">
+ <image id="1"
+ name="Image1"
+ updated="%(expected_now)s"
+ created="%(expected_now)s"
+ status="ACTIVE">
+ <server id="1">
+ <atom:link rel="self" href="%(expected_server_href)s"/>
+ <atom:link rel="bookmark" href="%(expected_server_bookmark)s"/>
+ </server>
+ <atom:link href="%(expected_href)s" rel="self"/>
+ <atom:link href="%(expected_bookmark)s" rel="bookmark"/>
+ </image>
+ <image id="2"
+ name="Image2"
+ updated="%(expected_now)s"
+ created="%(expected_now)s"
+ status="SAVING"
+ progress="80">
+ <metadata>
+ <meta key="key1">
+ value1
+ </meta>
+ </metadata>
+ <atom:link href="%(expected_href_two)s" rel="self"/>
+ <atom:link href="%(expected_bookmark_two)s" rel="bookmark"/>
+ </image>
</images>
""".replace(" ", "") % (locals()))
@@ -1446,16 +1674,32 @@ class ImageXMLSerializationTest(test.TestCase):
'name': 'Image1',
'created': self.TIMESTAMP,
'updated': self.TIMESTAMP,
- 'serverRef': self.SERVER_HREF,
- 'status': 'ACTIVE',
+ 'status': 'SAVING',
+ 'progress': 80,
+ 'server': {
+ 'id': 1,
+ 'links': [
+ {
+ 'href': self.SERVER_HREF,
+ 'rel': 'self',
+ },
+ {
+ 'href': self.SERVER_BOOKMARK,
+ 'rel': 'bookmark',
+ },
+ ],
+ },
'metadata': {
'key1': 'value1',
},
'links': [
{
- 'href': self.IMAGE_HREF % (1,),
+ 'href': self.IMAGE_HREF % 1,
+ 'rel': 'self',
+ },
+ {
+ 'href': self.IMAGE_BOOKMARK % 1,
'rel': 'bookmark',
- 'type': 'application/json',
},
],
},
@@ -1465,25 +1709,30 @@ class ImageXMLSerializationTest(test.TestCase):
actual = minidom.parseString(output.replace(" ", ""))
expected_server_href = self.SERVER_HREF
- expected_href = self.IMAGE_HREF % (1, )
+ expected_server_bookmark = self.SERVER_BOOKMARK
+ expected_href = self.IMAGE_HREF % 1
+ expected_bookmark = self.IMAGE_BOOKMARK % 1
expected_now = self.TIMESTAMP
expected = minidom.parseString("""
<image id="1"
+ xmlns="http://docs.openstack.org/compute/api/v1.1"
+ xmlns:atom="http://www.w3.org/2005/Atom"
name="Image1"
- serverRef="%(expected_server_href)s"
updated="%(expected_now)s"
created="%(expected_now)s"
- status="ACTIVE"
- xmlns="http://docs.openstack.org/compute/api/v1.1">
- <links>
- <link href="%(expected_href)s" rel="bookmark"
- type="application/json" />
- </links>
+ status="SAVING"
+ progress="80">
+ <server id="1">
+ <atom:link rel="self" href="%(expected_server_href)s"/>
+ <atom:link rel="bookmark" href="%(expected_server_bookmark)s"/>
+ </server>
<metadata>
<meta key="key1">
value1
</meta>
</metadata>
+ <atom:link href="%(expected_href)s" rel="self"/>
+ <atom:link href="%(expected_bookmark)s" rel="bookmark"/>
</image>
""".replace(" ", "") % (locals()))
diff --git a/nova/tests/api/openstack/test_limits.py b/nova/tests/api/openstack/test_limits.py
index 38c959fae..76363450d 100644
--- a/nova/tests/api/openstack/test_limits.py
+++ b/nova/tests/api/openstack/test_limits.py
@@ -400,6 +400,10 @@ class LimitsControllerV11Test(BaseLimitTestSuite):
self._test_index_absolute_limits_json(expected)
+class TestLimiter(limits.Limiter):
+ pass
+
+
class LimitMiddlewareTest(BaseLimitTestSuite):
"""
Tests for the `limits.RateLimitingMiddleware` class.
@@ -413,10 +417,14 @@ class LimitMiddlewareTest(BaseLimitTestSuite):
def setUp(self):
"""Prepare middleware for use through fake WSGI app."""
BaseLimitTestSuite.setUp(self)
- _limits = [
- limits.Limit("GET", "*", ".*", 1, 60),
- ]
- self.app = limits.RateLimitingMiddleware(self._empty_app, _limits)
+ _limits = '(GET, *, .*, 1, MINUTE)'
+ self.app = limits.RateLimitingMiddleware(self._empty_app, _limits,
+ "%s.TestLimiter" %
+ self.__class__.__module__)
+
+ def test_limit_class(self):
+ """Test that middleware selected correct limiter class."""
+ assert isinstance(self.app._limiter, TestLimiter)
def test_good_request(self):
"""Test successful GET request through middleware."""
@@ -492,6 +500,72 @@ class LimitTest(BaseLimitTestSuite):
self.assertEqual(4, limit.last_request)
+class ParseLimitsTest(BaseLimitTestSuite):
+ """
+ Tests for the default limits parser in the in-memory
+ `limits.Limiter` class.
+ """
+
+ def test_invalid(self):
+ """Test that parse_limits() handles invalid input correctly."""
+ self.assertRaises(ValueError, limits.Limiter.parse_limits,
+ ';;;;;')
+
+ def test_bad_rule(self):
+ """Test that parse_limits() handles bad rules correctly."""
+ self.assertRaises(ValueError, limits.Limiter.parse_limits,
+ 'GET, *, .*, 20, minute')
+
+ def test_missing_arg(self):
+ """Test that parse_limits() handles missing args correctly."""
+ self.assertRaises(ValueError, limits.Limiter.parse_limits,
+ '(GET, *, .*, 20)')
+
+ def test_bad_value(self):
+ """Test that parse_limits() handles bad values correctly."""
+ self.assertRaises(ValueError, limits.Limiter.parse_limits,
+ '(GET, *, .*, foo, minute)')
+
+ def test_bad_unit(self):
+ """Test that parse_limits() handles bad units correctly."""
+ self.assertRaises(ValueError, limits.Limiter.parse_limits,
+ '(GET, *, .*, 20, lightyears)')
+
+ def test_multiple_rules(self):
+ """Test that parse_limits() handles multiple rules correctly."""
+ try:
+ l = limits.Limiter.parse_limits('(get, *, .*, 20, minute);'
+ '(PUT, /foo*, /foo.*, 10, hour);'
+ '(POST, /bar*, /bar.*, 5, second);'
+ '(Say, /derp*, /derp.*, 1, day)')
+ except ValueError, e:
+ assert False, str(e)
+
+ # Make sure the number of returned limits are correct
+ self.assertEqual(len(l), 4)
+
+ # Check all the verbs...
+ expected = ['GET', 'PUT', 'POST', 'SAY']
+ self.assertEqual([t.verb for t in l], expected)
+
+ # ...the URIs...
+ expected = ['*', '/foo*', '/bar*', '/derp*']
+ self.assertEqual([t.uri for t in l], expected)
+
+ # ...the regexes...
+ expected = ['.*', '/foo.*', '/bar.*', '/derp.*']
+ self.assertEqual([t.regex for t in l], expected)
+
+ # ...the values...
+ expected = [20, 10, 5, 1]
+ self.assertEqual([t.value for t in l], expected)
+
+ # ...and the units...
+ expected = [limits.PER_MINUTE, limits.PER_HOUR,
+ limits.PER_SECOND, limits.PER_DAY]
+ self.assertEqual([t.unit for t in l], expected)
+
+
class LimiterTest(BaseLimitTestSuite):
"""
Tests for the in-memory `limits.Limiter` class.
@@ -500,7 +574,8 @@ class LimiterTest(BaseLimitTestSuite):
def setUp(self):
"""Run before each test."""
BaseLimitTestSuite.setUp(self)
- self.limiter = limits.Limiter(TEST_LIMITS)
+ userlimits = {'user:user3': ''}
+ self.limiter = limits.Limiter(TEST_LIMITS, **userlimits)
def _check(self, num, verb, url, username=None):
"""Check and yield results from checks."""
@@ -605,6 +680,12 @@ class LimiterTest(BaseLimitTestSuite):
results = list(self._check(10, "PUT", "/anything"))
self.assertEqual(expected, results)
+ def test_user_limit(self):
+ """
+ Test user-specific limits.
+ """
+ self.assertEqual(self.limiter.levels['user3'], [])
+
def test_multiple_users(self):
"""
Tests involving multiple users.
@@ -619,6 +700,11 @@ class LimiterTest(BaseLimitTestSuite):
results = list(self._check(15, "PUT", "/anything", "user2"))
self.assertEqual(expected, results)
+ # User3
+ expected = [None] * 20
+ results = list(self._check(20, "PUT", "/anything", "user3"))
+ self.assertEqual(expected, results)
+
self.time += 1.0
# User1 again
diff --git a/nova/tests/api/openstack/test_servers.py b/nova/tests/api/openstack/test_servers.py
index 1f369c4c8..1577c922b 100644
--- a/nova/tests/api/openstack/test_servers.py
+++ b/nova/tests/api/openstack/test_servers.py
@@ -65,6 +65,18 @@ def return_server_by_uuid(context, uuid):
return stub_instance(id, uuid=uuid)
+def return_virtual_interface_by_instance(interfaces):
+ def _return_virtual_interface_by_instance(context, instance_id):
+ return interfaces
+ return _return_virtual_interface_by_instance
+
+
+def return_virtual_interface_instance_nonexistant(interfaces):
+ def _return_virtual_interface_by_instance(context, instance_id):
+ raise exception.InstanceNotFound(instance_id=instance_id)
+ return _return_virtual_interface_by_instance
+
+
def return_server_with_addresses(private, public):
def _return_server(context, id):
return stub_instance(id, private_address=private,
@@ -72,6 +84,12 @@ def return_server_with_addresses(private, public):
return _return_server
+def return_server_with_interfaces(interfaces):
+ def _return_server(context, id):
+ return stub_instance(id, interfaces=interfaces)
+ return _return_server
+
+
def return_server_with_power_state(power_state):
def _return_server(context, id):
return stub_instance(id, power_state=power_state)
@@ -124,10 +142,13 @@ def instance_addresses(context, instance_id):
def stub_instance(id, user_id=1, private_address=None, public_addresses=None,
host=None, power_state=0, reservation_id="",
- uuid=FAKE_UUID):
+ uuid=FAKE_UUID, interfaces=None):
metadata = []
metadata.append(InstanceMetadata(key='seq', value=id))
+ if interfaces is None:
+ interfaces = []
+
inst_type = instance_types.get_instance_type_by_flavor_id(1)
if public_addresses is None:
@@ -171,7 +192,8 @@ def stub_instance(id, user_id=1, private_address=None, public_addresses=None,
"display_description": "",
"locked": False,
"metadata": metadata,
- "uuid": uuid}
+ "uuid": uuid,
+ "virtual_interfaces": interfaces}
instance["fixed_ips"] = {
"address": private_address,
@@ -411,23 +433,152 @@ class ServersTest(test.TestCase):
self.assertEquals(ip.getAttribute('addr'), private)
def test_get_server_by_id_with_addresses_v1_1(self):
- private = "192.168.0.3"
- public = ["1.2.3.4"]
- new_return_server = return_server_with_addresses(private, public)
+ interfaces = [
+ {
+ 'network': {'label': 'network_1'},
+ 'fixed_ips': [
+ {'address': '192.168.0.3'},
+ {'address': '192.168.0.4'},
+ ],
+ },
+ {
+ 'network': {'label': 'network_2'},
+ 'fixed_ips': [
+ {'address': '172.19.0.1'},
+ {'address': '172.19.0.2'},
+ ],
+ },
+ ]
+ new_return_server = return_server_with_interfaces(interfaces)
self.stubs.Set(nova.db.api, 'instance_get', new_return_server)
+
req = webob.Request.blank('/v1.1/servers/1')
res = req.get_response(fakes.wsgi_app())
+
res_dict = json.loads(res.body)
self.assertEqual(res_dict['server']['id'], 1)
self.assertEqual(res_dict['server']['name'], 'server1')
addresses = res_dict['server']['addresses']
- # RM(4047): Figure otu what is up with the 1.1 api and multi-nic
- #self.assertEqual(len(addresses["public"]), len(public))
- #self.assertEqual(addresses["public"][0],
- # {"version": 4, "addr": public[0]})
- #self.assertEqual(len(addresses["private"]), 1)
- #self.assertEqual(addresses["private"][0],
- # {"version": 4, "addr": private})
+ expected = {
+ 'network_1': [
+ {'addr': '192.168.0.3', 'version': 4},
+ {'addr': '192.168.0.4', 'version': 4},
+ ],
+ 'network_2': [
+ {'addr': '172.19.0.1', 'version': 4},
+ {'addr': '172.19.0.2', 'version': 4},
+ ],
+ }
+
+ self.assertEqual(addresses, expected)
+
+ def test_get_server_addresses_v1_1(self):
+ interfaces = [
+ {
+ 'network': {'label': 'network_1'},
+ 'fixed_ips': [
+ {'address': '192.168.0.3'},
+ {'address': '192.168.0.4'},
+ ],
+ },
+ {
+ 'network': {'label': 'network_2'},
+ 'fixed_ips': [
+ {
+ 'address': '172.19.0.1',
+ 'floating_ips': [
+ {'address': '1.2.3.4'},
+ ],
+ },
+ {'address': '172.19.0.2'},
+ ],
+ },
+ ]
+
+ _return_vifs = return_virtual_interface_by_instance(interfaces)
+ self.stubs.Set(nova.db.api,
+ 'virtual_interface_get_by_instance',
+ _return_vifs)
+
+ req = webob.Request.blank('/v1.1/servers/1/ips')
+ res = req.get_response(fakes.wsgi_app())
+ res_dict = json.loads(res.body)
+
+ expected = {
+ 'addresses': {
+ 'network_1': [
+ {'version': 4, 'addr': '192.168.0.3'},
+ {'version': 4, 'addr': '192.168.0.4'},
+ ],
+ 'network_2': [
+ {'version': 4, 'addr': '172.19.0.1'},
+ {'version': 4, 'addr': '1.2.3.4'},
+ {'version': 4, 'addr': '172.19.0.2'},
+ ],
+ },
+ }
+
+ self.assertEqual(res_dict, expected)
+
+ def test_get_server_addresses_single_network_v1_1(self):
+ interfaces = [
+ {
+ 'network': {'label': 'network_1'},
+ 'fixed_ips': [
+ {'address': '192.168.0.3'},
+ {'address': '192.168.0.4'},
+ ],
+ },
+ {
+ 'network': {'label': 'network_2'},
+ 'fixed_ips': [
+ {
+ 'address': '172.19.0.1',
+ 'floating_ips': [
+ {'address': '1.2.3.4'},
+ ],
+ },
+ {'address': '172.19.0.2'},
+ ],
+ },
+ ]
+ _return_vifs = return_virtual_interface_by_instance(interfaces)
+ self.stubs.Set(nova.db.api,
+ 'virtual_interface_get_by_instance',
+ _return_vifs)
+
+ req = webob.Request.blank('/v1.1/servers/1/ips/network_2')
+ res = req.get_response(fakes.wsgi_app())
+ self.assertEqual(res.status_int, 200)
+ res_dict = json.loads(res.body)
+ expected = {
+ 'network_2': [
+ {'version': 4, 'addr': '172.19.0.1'},
+ {'version': 4, 'addr': '1.2.3.4'},
+ {'version': 4, 'addr': '172.19.0.2'},
+ ],
+ }
+ self.assertEqual(res_dict, expected)
+
+ def test_get_server_addresses_nonexistant_network_v1_1(self):
+ _return_vifs = return_virtual_interface_by_instance([])
+ self.stubs.Set(nova.db.api,
+ 'virtual_interface_get_by_instance',
+ _return_vifs)
+
+ req = webob.Request.blank('/v1.1/servers/1/ips/network_0')
+ res = req.get_response(fakes.wsgi_app())
+ self.assertEqual(res.status_int, 404)
+
+ def test_get_server_addresses_nonexistant_server_v1_1(self):
+ _return_vifs = return_virtual_interface_instance_nonexistant([])
+ self.stubs.Set(nova.db.api,
+ 'virtual_interface_get_by_instance',
+ _return_vifs)
+
+ req = webob.Request.blank('/v1.1/servers/600/ips')
+ res = req.get_response(fakes.wsgi_app())
+ self.assertEqual(res.status_int, 404)
def test_get_server_list(self):
req = webob.Request.blank('/v1.0/servers')
@@ -787,13 +938,13 @@ class ServersTest(test.TestCase):
res = req.get_response(fakes.wsgi_app())
+ self.assertEqual(res.status_int, 200)
server = json.loads(res.body)['server']
self.assertEqual(16, len(server['adminPass']))
self.assertEqual('server_test', server['name'])
self.assertEqual(1, server['id'])
self.assertEqual(flavor_ref, server['flavorRef'])
self.assertEqual(image_href, server['imageRef'])
- self.assertEqual(res.status_int, 200)
def test_create_instance_v1_1_bad_href(self):
self._setup_for_create_instance()
@@ -901,11 +1052,11 @@ class ServersTest(test.TestCase):
res = req.get_response(fakes.wsgi_app())
self.assertEqual(res.status_int, 400)
- def test_update_no_body(self):
+ def test_update_server_no_body(self):
req = webob.Request.blank('/v1.0/servers/1')
req.method = 'PUT'
res = req.get_response(fakes.wsgi_app())
- self.assertEqual(res.status_int, 422)
+ self.assertEqual(res.status_int, 400)
def test_update_nonstring_name(self):
""" Confirm that update is filtering params """
@@ -967,6 +1118,21 @@ class ServersTest(test.TestCase):
self.assertEqual(mock_method.instance_id, '1')
self.assertEqual(mock_method.password, 'bacon')
+ def test_update_server_no_body_v1_1(self):
+ req = webob.Request.blank('/v1.0/servers/1')
+ req.method = 'PUT'
+ res = req.get_response(fakes.wsgi_app())
+ self.assertEqual(res.status_int, 400)
+
+ def test_update_server_name_v1_1(self):
+ req = webob.Request.blank('/v1.1/servers/1')
+ req.method = 'PUT'
+ req.content_type = 'application/json'
+ req.body = json.dumps({'server': {'name': 'new-name'}})
+ res = req.get_response(fakes.wsgi_app())
+ self.assertEqual(res.status_int, 204)
+ self.assertEqual(res.body, '')
+
def test_update_server_adminPass_ignored_v1_1(self):
inst_dict = dict(name='server_test', adminPass='bacon')
self.body = json.dumps(dict(server=inst_dict))
@@ -985,6 +1151,7 @@ class ServersTest(test.TestCase):
req.body = self.body
res = req.get_response(fakes.wsgi_app())
self.assertEqual(res.status_int, 204)
+ self.assertEqual(res.body, '')
def test_create_backup_schedules(self):
req = webob.Request.blank('/v1.0/servers/1/backup_schedule')
@@ -1433,6 +1600,57 @@ class ServersTest(test.TestCase):
self.assertEqual(res.status, '202 Accepted')
self.assertEqual(self.server_delete_called, True)
+ def test_rescue_accepted(self):
+ FLAGS.allow_admin_api = True
+ body = {}
+
+ self.called = False
+
+ def rescue_mock(*args, **kwargs):
+ self.called = True
+
+ self.stubs.Set(nova.compute.api.API, 'rescue', rescue_mock)
+ req = webob.Request.blank('/v1.0/servers/1/rescue')
+ req.method = 'POST'
+ req.content_type = 'application/json'
+
+ res = req.get_response(fakes.wsgi_app())
+
+ self.assertEqual(self.called, True)
+ self.assertEqual(res.status_int, 202)
+
+ def test_rescue_raises_handled(self):
+ FLAGS.allow_admin_api = True
+ body = {}
+
+ def rescue_mock(*args, **kwargs):
+ raise Exception('Who cares?')
+
+ self.stubs.Set(nova.compute.api.API, 'rescue', rescue_mock)
+ req = webob.Request.blank('/v1.0/servers/1/rescue')
+ req.method = 'POST'
+ req.content_type = 'application/json'
+
+ res = req.get_response(fakes.wsgi_app())
+
+ self.assertEqual(res.status_int, 422)
+
+ def test_delete_server_instance_v1_1(self):
+ req = webob.Request.blank('/v1.1/servers/1')
+ req.method = 'DELETE'
+
+ self.server_delete_called = False
+
+ def instance_destroy_mock(context, id):
+ self.server_delete_called = True
+
+ self.stubs.Set(nova.db.api, 'instance_destroy',
+ instance_destroy_mock)
+
+ res = req.get_response(fakes.wsgi_app())
+ 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)))
@@ -1608,7 +1826,7 @@ class TestServerCreateRequestXMLDeserializer(unittest.TestCase):
"imageId": "1",
"flavorId": "1",
}}
- self.assertEquals(request, expected)
+ self.assertEquals(request['body'], expected)
def test_request_with_empty_metadata(self):
serial_request = """
@@ -1623,7 +1841,7 @@ class TestServerCreateRequestXMLDeserializer(unittest.TestCase):
"flavorId": "1",
"metadata": {},
}}
- self.assertEquals(request, expected)
+ self.assertEquals(request['body'], expected)
def test_request_with_empty_personality(self):
serial_request = """
@@ -1638,7 +1856,7 @@ class TestServerCreateRequestXMLDeserializer(unittest.TestCase):
"flavorId": "1",
"personality": [],
}}
- self.assertEquals(request, expected)
+ self.assertEquals(request['body'], expected)
def test_request_with_empty_metadata_and_personality(self):
serial_request = """
@@ -1655,7 +1873,7 @@ class TestServerCreateRequestXMLDeserializer(unittest.TestCase):
"metadata": {},
"personality": [],
}}
- self.assertEquals(request, expected)
+ self.assertEquals(request['body'], expected)
def test_request_with_empty_metadata_and_personality_reversed(self):
serial_request = """
@@ -1672,7 +1890,7 @@ class TestServerCreateRequestXMLDeserializer(unittest.TestCase):
"metadata": {},
"personality": [],
}}
- self.assertEquals(request, expected)
+ self.assertEquals(request['body'], expected)
def test_request_with_one_personality(self):
serial_request = """
@@ -1684,7 +1902,7 @@ class TestServerCreateRequestXMLDeserializer(unittest.TestCase):
</server>"""
request = self.deserializer.deserialize(serial_request, 'create')
expected = [{"path": "/etc/conf", "contents": "aabbccdd"}]
- self.assertEquals(request["server"]["personality"], expected)
+ self.assertEquals(request['body']["server"]["personality"], expected)
def test_request_with_two_personalities(self):
serial_request = """
@@ -1695,7 +1913,7 @@ class TestServerCreateRequestXMLDeserializer(unittest.TestCase):
request = self.deserializer.deserialize(serial_request, 'create')
expected = [{"path": "/etc/conf", "contents": "aabbccdd"},
{"path": "/etc/sudoers", "contents": "abcd"}]
- self.assertEquals(request["server"]["personality"], expected)
+ self.assertEquals(request['body']["server"]["personality"], expected)
def test_request_second_personality_node_ignored(self):
serial_request = """
@@ -1710,7 +1928,7 @@ class TestServerCreateRequestXMLDeserializer(unittest.TestCase):
</server>"""
request = self.deserializer.deserialize(serial_request, 'create')
expected = [{"path": "/etc/conf", "contents": "aabbccdd"}]
- self.assertEquals(request["server"]["personality"], expected)
+ self.assertEquals(request['body']["server"]["personality"], expected)
def test_request_with_one_personality_missing_path(self):
serial_request = """
@@ -1719,7 +1937,7 @@ class TestServerCreateRequestXMLDeserializer(unittest.TestCase):
<personality><file>aabbccdd</file></personality></server>"""
request = self.deserializer.deserialize(serial_request, 'create')
expected = [{"contents": "aabbccdd"}]
- self.assertEquals(request["server"]["personality"], expected)
+ self.assertEquals(request['body']["server"]["personality"], expected)
def test_request_with_one_personality_empty_contents(self):
serial_request = """
@@ -1728,7 +1946,7 @@ class TestServerCreateRequestXMLDeserializer(unittest.TestCase):
<personality><file path="/etc/conf"></file></personality></server>"""
request = self.deserializer.deserialize(serial_request, 'create')
expected = [{"path": "/etc/conf", "contents": ""}]
- self.assertEquals(request["server"]["personality"], expected)
+ self.assertEquals(request['body']["server"]["personality"], expected)
def test_request_with_one_personality_empty_contents_variation(self):
serial_request = """
@@ -1737,7 +1955,7 @@ class TestServerCreateRequestXMLDeserializer(unittest.TestCase):
<personality><file path="/etc/conf"/></personality></server>"""
request = self.deserializer.deserialize(serial_request, 'create')
expected = [{"path": "/etc/conf", "contents": ""}]
- self.assertEquals(request["server"]["personality"], expected)
+ self.assertEquals(request['body']["server"]["personality"], expected)
def test_request_with_one_metadata(self):
serial_request = """
@@ -1749,7 +1967,7 @@ class TestServerCreateRequestXMLDeserializer(unittest.TestCase):
</server>"""
request = self.deserializer.deserialize(serial_request, 'create')
expected = {"alpha": "beta"}
- self.assertEquals(request["server"]["metadata"], expected)
+ self.assertEquals(request['body']["server"]["metadata"], expected)
def test_request_with_two_metadata(self):
serial_request = """
@@ -1762,7 +1980,7 @@ class TestServerCreateRequestXMLDeserializer(unittest.TestCase):
</server>"""
request = self.deserializer.deserialize(serial_request, 'create')
expected = {"alpha": "beta", "foo": "bar"}
- self.assertEquals(request["server"]["metadata"], expected)
+ self.assertEquals(request['body']["server"]["metadata"], expected)
def test_request_with_metadata_missing_value(self):
serial_request = """
@@ -1774,7 +1992,7 @@ class TestServerCreateRequestXMLDeserializer(unittest.TestCase):
</server>"""
request = self.deserializer.deserialize(serial_request, 'create')
expected = {"alpha": ""}
- self.assertEquals(request["server"]["metadata"], expected)
+ self.assertEquals(request['body']["server"]["metadata"], expected)
def test_request_with_two_metadata_missing_value(self):
serial_request = """
@@ -1787,7 +2005,7 @@ class TestServerCreateRequestXMLDeserializer(unittest.TestCase):
</server>"""
request = self.deserializer.deserialize(serial_request, 'create')
expected = {"alpha": "", "delta": ""}
- self.assertEquals(request["server"]["metadata"], expected)
+ self.assertEquals(request['body']["server"]["metadata"], expected)
def test_request_with_metadata_missing_key(self):
serial_request = """
@@ -1799,7 +2017,7 @@ class TestServerCreateRequestXMLDeserializer(unittest.TestCase):
</server>"""
request = self.deserializer.deserialize(serial_request, 'create')
expected = {"": "beta"}
- self.assertEquals(request["server"]["metadata"], expected)
+ self.assertEquals(request['body']["server"]["metadata"], expected)
def test_request_with_two_metadata_missing_key(self):
serial_request = """
@@ -1812,7 +2030,7 @@ class TestServerCreateRequestXMLDeserializer(unittest.TestCase):
</server>"""
request = self.deserializer.deserialize(serial_request, 'create')
expected = {"": "gamma"}
- self.assertEquals(request["server"]["metadata"], expected)
+ self.assertEquals(request['body']["server"]["metadata"], expected)
def test_request_with_metadata_duplicate_key(self):
serial_request = """
@@ -1825,7 +2043,7 @@ class TestServerCreateRequestXMLDeserializer(unittest.TestCase):
</server>"""
request = self.deserializer.deserialize(serial_request, 'create')
expected = {"foo": "baz"}
- self.assertEquals(request["server"]["metadata"], expected)
+ self.assertEquals(request['body']["server"]["metadata"], expected)
def test_canonical_request_from_docs(self):
serial_request = """
@@ -1871,7 +2089,7 @@ b25zLiINCg0KLVJpY2hhcmQgQmFjaA==""",
],
}}
request = self.deserializer.deserialize(serial_request, 'create')
- self.assertEqual(request, expected)
+ self.assertEqual(request['body'], expected)
def test_request_xmlser_with_flavor_image_href(self):
serial_request = """
@@ -1881,9 +2099,9 @@ b25zLiINCg0KLVJpY2hhcmQgQmFjaA==""",
flavorRef="http://localhost:8774/v1.1/flavors/1">
</server>"""
request = self.deserializer.deserialize(serial_request, 'create')
- self.assertEquals(request["server"]["flavorRef"],
+ self.assertEquals(request['body']["server"]["flavorRef"],
"http://localhost:8774/v1.1/flavors/1")
- self.assertEquals(request["server"]["imageRef"],
+ self.assertEquals(request['body']["server"]["imageRef"],
"http://localhost:8774/v1.1/images/1")
@@ -1948,7 +2166,7 @@ class TestServerInstanceCreation(test.TestCase):
def _get_create_request_json(self, body_dict):
req = webob.Request.blank('/v1.0/servers')
- req.content_type = 'application/json'
+ req.headers['Content-Type'] = 'application/json'
req.method = 'POST'
req.body = json.dumps(body_dict)
return req
diff --git a/nova/tests/api/openstack/test_wsgi.py b/nova/tests/api/openstack/test_wsgi.py
index 73a26a087..5bdda7c7e 100644
--- a/nova/tests/api/openstack/test_wsgi.py
+++ b/nova/tests/api/openstack/test_wsgi.py
@@ -12,8 +12,7 @@ class RequestTest(test.TestCase):
def test_content_type_missing(self):
request = wsgi.Request.blank('/tests/123', method='POST')
request.body = "<body />"
- self.assertRaises(exception.InvalidContentType,
- request.get_content_type)
+ self.assertEqual(None, request.get_content_type())
def test_content_type_unsupported(self):
request = wsgi.Request.blank('/tests/123', method='POST')
@@ -76,24 +75,48 @@ class RequestTest(test.TestCase):
self.assertEqual(result, "application/json")
-class DictSerializerTest(test.TestCase):
+class ActionDispatcherTest(test.TestCase):
def test_dispatch(self):
- serializer = wsgi.DictSerializer()
+ serializer = wsgi.ActionDispatcher()
+ serializer.create = lambda x: 'pants'
+ self.assertEqual(serializer.dispatch({}, action='create'), 'pants')
+
+ def test_dispatch_action_None(self):
+ serializer = wsgi.ActionDispatcher()
serializer.create = lambda x: 'pants'
serializer.default = lambda x: 'trousers'
- self.assertEqual(serializer.serialize({}, 'create'), 'pants')
+ self.assertEqual(serializer.dispatch({}, action=None), 'trousers')
def test_dispatch_default(self):
- serializer = wsgi.DictSerializer()
+ serializer = wsgi.ActionDispatcher()
serializer.create = lambda x: 'pants'
serializer.default = lambda x: 'trousers'
- self.assertEqual(serializer.serialize({}, 'update'), 'trousers')
+ self.assertEqual(serializer.dispatch({}, action='update'), 'trousers')
- def test_dispatch_action_None(self):
+
+class ResponseHeadersSerializerTest(test.TestCase):
+ def test_default(self):
+ serializer = wsgi.ResponseHeadersSerializer()
+ response = webob.Response()
+ serializer.serialize(response, {'v': '123'}, 'asdf')
+ self.assertEqual(response.status_int, 200)
+
+ def test_custom(self):
+ class Serializer(wsgi.ResponseHeadersSerializer):
+ def update(self, response, data):
+ response.status_int = 404
+ response.headers['X-Custom-Header'] = data['v']
+ serializer = Serializer()
+ response = webob.Response()
+ serializer.serialize(response, {'v': '123'}, 'update')
+ self.assertEqual(response.status_int, 404)
+ self.assertEqual(response.headers['X-Custom-Header'], '123')
+
+
+class DictSerializerTest(test.TestCase):
+ def test_dispatch_default(self):
serializer = wsgi.DictSerializer()
- serializer.create = lambda x: 'pants'
- serializer.default = lambda x: 'trousers'
- self.assertEqual(serializer.serialize({}, None), 'trousers')
+ self.assertEqual(serializer.serialize({}, 'update'), '')
class XMLDictSerializerTest(test.TestCase):
@@ -117,23 +140,9 @@ class JSONDictSerializerTest(test.TestCase):
class TextDeserializerTest(test.TestCase):
- def test_dispatch(self):
- deserializer = wsgi.TextDeserializer()
- deserializer.create = lambda x: 'pants'
- deserializer.default = lambda x: 'trousers'
- self.assertEqual(deserializer.deserialize({}, 'create'), 'pants')
-
def test_dispatch_default(self):
deserializer = wsgi.TextDeserializer()
- deserializer.create = lambda x: 'pants'
- deserializer.default = lambda x: 'trousers'
- self.assertEqual(deserializer.deserialize({}, 'update'), 'trousers')
-
- def test_dispatch_action_None(self):
- deserializer = wsgi.TextDeserializer()
- deserializer.create = lambda x: 'pants'
- deserializer.default = lambda x: 'trousers'
- self.assertEqual(deserializer.deserialize({}, None), 'trousers')
+ self.assertEqual(deserializer.deserialize({}, 'update'), {})
class JSONDeserializerTest(test.TestCase):
@@ -144,12 +153,17 @@ class JSONDeserializerTest(test.TestCase):
"bs": ["1", "2", "3", {"c": {"c1": "1"}}],
"d": {"e": "1"},
"f": "1"}}"""
- as_dict = dict(a={
- 'a1': '1',
- 'a2': '2',
- 'bs': ['1', '2', '3', {'c': dict(c1='1')}],
- 'd': {'e': '1'},
- 'f': '1'})
+ as_dict = {
+ 'body': {
+ 'a': {
+ 'a1': '1',
+ 'a2': '2',
+ 'bs': ['1', '2', '3', {'c': {'c1': '1'}}],
+ 'd': {'e': '1'},
+ 'f': '1',
+ },
+ },
+ }
deserializer = wsgi.JSONDeserializer()
self.assertEqual(deserializer.deserialize(data), as_dict)
@@ -163,23 +177,44 @@ class XMLDeserializerTest(test.TestCase):
<f>1</f>
</a>
""".strip()
- as_dict = dict(a={
- 'a1': '1',
- 'a2': '2',
- 'bs': ['1', '2', '3', {'c': dict(c1='1')}],
- 'd': {'e': '1'},
- 'f': '1'})
+ as_dict = {
+ 'body': {
+ 'a': {
+ 'a1': '1',
+ 'a2': '2',
+ 'bs': ['1', '2', '3', {'c': {'c1': '1'}}],
+ 'd': {'e': '1'},
+ 'f': '1',
+ },
+ },
+ }
metadata = {'plurals': {'bs': 'b', 'ts': 't'}}
deserializer = wsgi.XMLDeserializer(metadata=metadata)
self.assertEqual(deserializer.deserialize(xml), as_dict)
def test_xml_empty(self):
xml = """<a></a>"""
- as_dict = {"a": {}}
+ as_dict = {"body": {"a": {}}}
deserializer = wsgi.XMLDeserializer()
self.assertEqual(deserializer.deserialize(xml), as_dict)
+class RequestHeadersDeserializerTest(test.TestCase):
+ def test_default(self):
+ deserializer = wsgi.RequestHeadersDeserializer()
+ req = wsgi.Request.blank('/')
+ self.assertEqual(deserializer.deserialize(req, 'asdf'), {})
+
+ def test_custom(self):
+ class Deserializer(wsgi.RequestHeadersDeserializer):
+ def update(self, request):
+ return {'a': request.headers['X-Custom-Header']}
+ deserializer = Deserializer()
+ req = wsgi.Request.blank('/')
+ req.headers['X-Custom-Header'] = 'b'
+ self.assertEqual(deserializer.deserialize(req, 'update'), {'a': 'b'})
+
+
class ResponseSerializerTest(test.TestCase):
def setUp(self):
class JSONSerializer(object):
@@ -190,29 +225,36 @@ class ResponseSerializerTest(test.TestCase):
def serialize(self, data, action='default'):
return 'pew_xml'
- self.serializers = {
+ class HeadersSerializer(object):
+ def serialize(self, response, data, action):
+ response.status_int = 404
+
+ self.body_serializers = {
'application/json': JSONSerializer(),
'application/XML': XMLSerializer(),
}
- self.serializer = wsgi.ResponseSerializer(serializers=self.serializers)
+ self.serializer = wsgi.ResponseSerializer(self.body_serializers,
+ HeadersSerializer())
def tearDown(self):
pass
def test_get_serializer(self):
- self.assertEqual(self.serializer.get_serializer('application/json'),
- self.serializers['application/json'])
+ ctype = 'application/json'
+ self.assertEqual(self.serializer.get_body_serializer(ctype),
+ self.body_serializers[ctype])
def test_get_serializer_unknown_content_type(self):
self.assertRaises(exception.InvalidContentType,
- self.serializer.get_serializer,
+ self.serializer.get_body_serializer,
'application/unknown')
def test_serialize_response(self):
response = self.serializer.serialize({}, 'application/json')
self.assertEqual(response.headers['Content-Type'], 'application/json')
self.assertEqual(response.body, 'pew_json')
+ self.assertEqual(response.status_int, 404)
def test_serialize_response_dict_to_unknown_content_type(self):
self.assertRaises(exception.InvalidContentType,
@@ -230,24 +272,23 @@ class RequestDeserializerTest(test.TestCase):
def deserialize(self, data, action='default'):
return 'pew_xml'
- self.deserializers = {
+ self.body_deserializers = {
'application/json': JSONDeserializer(),
'application/XML': XMLDeserializer(),
}
- self.deserializer = wsgi.RequestDeserializer(
- deserializers=self.deserializers)
+ self.deserializer = wsgi.RequestDeserializer(self.body_deserializers)
def tearDown(self):
pass
def test_get_deserializer(self):
- expected = self.deserializer.get_deserializer('application/json')
- self.assertEqual(expected, self.deserializers['application/json'])
+ expected = self.deserializer.get_body_deserializer('application/json')
+ self.assertEqual(expected, self.body_deserializers['application/json'])
def test_get_deserializer_unknown_content_type(self):
self.assertRaises(exception.InvalidContentType,
- self.deserializer.get_deserializer,
+ self.deserializer.get_body_deserializer,
'application/unknown')
def test_get_expected_content_type(self):
diff --git a/nova/tests/image/test_s3.py b/nova/tests/image/test_s3.py
new file mode 100644
index 000000000..231e109f8
--- /dev/null
+++ b/nova/tests/image/test_s3.py
@@ -0,0 +1,122 @@
+# 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 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>
+ <version>2011-06-17</version>
+ <bundler>
+ <name>test-s3</name>
+ <version>0</version>
+ <release>0</release>
+ </bundler>
+ <machine_configuration>
+ <architecture>x86_64</architecture>
+ <block_device_mapping>
+ <mapping>
+ <virtual>ami</virtual>
+ <device>sda1</device>
+ </mapping>
+ <mapping>
+ <virtual>root</virtual>
+ <device>/dev/sda1</device>
+ </mapping>
+ <mapping>
+ <virtual>ephemeral0</virtual>
+ <device>sda2</device>
+ </mapping>
+ <mapping>
+ <virtual>swap</virtual>
+ <device>sda3</device>
+ </mapping>
+ </block_device_mapping>
+ </machine_configuration>
+</manifest>
+"""
+
+
+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.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]
+ for x in list0:
+ self.assertEqual(len(x), len(keys))
+ self.assertTrue(key in x)
+ for y in list1:
+ self.assertTrue(key in y)
+ if x[key] == y[key]:
+ for k in keys:
+ self.assertEqual(x[k], y[k])
+
+ def test_s3_create(self):
+ metadata = {'properties': {
+ 'root_device_name': '/dev/sda1',
+ 'block_device_mapping': [
+ {'device_name': '/dev/sda1',
+ 'snapshot_id': 'snap-12345678',
+ 'delete_on_termination': True},
+ {'device_name': '/dev/sda2',
+ 'virutal_name': 'ephemeral0'},
+ {'device_name': '/dev/sdb0',
+ 'no_device': True}]}}
+ _manifest, image = self.image_service._s3_parse_manifest(
+ self.context, metadata, ami_manifest_xml)
+ image_id = image['id']
+
+ ret_image = self.image_service.show(self.context, image_id)
+ self.assertTrue('properties' in ret_image)
+ properties = ret_image['properties']
+
+ self.assertTrue('mappings' in properties)
+ mappings = properties['mappings']
+ expected_mappings = [
+ {"device": "sda1", "virtual": "ami"},
+ {"device": "/dev/sda1", "virtual": "root"},
+ {"device": "sda2", "virtual": "ephemeral0"},
+ {"device": "sda3", "virtual": "swap"}]
+ self._assertEqualList(mappings, expected_mappings,
+ ['device', 'virtual'])
+
+ self.assertTrue('block_device_mapping', properties)
+ block_device_mapping = properties['block_device_mapping']
+ expected_bdm = [
+ {'device_name': '/dev/sda1',
+ 'snapshot_id': 'snap-12345678',
+ 'delete_on_termination': True},
+ {'device_name': '/dev/sda2',
+ 'virutal_name': 'ephemeral0'},
+ {'device_name': '/dev/sdb0',
+ 'no_device': True}]
+ self.assertEqual(block_device_mapping, expected_bdm)
diff --git a/nova/tests/integrated/api/client.py b/nova/tests/integrated/api/client.py
index 76c03c5fa..035a35aab 100644
--- a/nova/tests/integrated/api/client.py
+++ b/nova/tests/integrated/api/client.py
@@ -71,8 +71,8 @@ class TestOpenStackClient(object):
self.auth_uri = auth_uri
def request(self, url, method='GET', body=None, headers=None):
- if headers is None:
- headers = {}
+ _headers = {'Content-Type': 'application/json'}
+ _headers.update(headers or {})
parsed_url = urlparse.urlparse(url)
port = parsed_url.port
@@ -94,9 +94,8 @@ class TestOpenStackClient(object):
LOG.info(_("Doing %(method)s on %(relative_url)s") % locals())
if body:
LOG.info(_("Body: %s") % body)
- headers.setdefault('Content-Type', 'application/json')
- conn.request(method, relative_url, body, headers)
+ conn.request(method, relative_url, body, _headers)
response = conn.getresponse()
return response
@@ -173,9 +172,20 @@ class TestOpenStackClient(object):
response = self.api_request(relative_uri, **kwargs)
return self._decode_json(response)
+ def api_put(self, relative_uri, body, **kwargs):
+ kwargs['method'] = 'PUT'
+ if body:
+ headers = kwargs.setdefault('headers', {})
+ headers['Content-Type'] = 'application/json'
+ kwargs['body'] = json.dumps(body)
+
+ kwargs.setdefault('check_response_status', [200, 202, 204])
+ response = self.api_request(relative_uri, **kwargs)
+ return self._decode_json(response)
+
def api_delete(self, relative_uri, **kwargs):
kwargs['method'] = 'DELETE'
- kwargs.setdefault('check_response_status', [200, 202])
+ kwargs.setdefault('check_response_status', [200, 202, 204])
return self.api_request(relative_uri, **kwargs)
def get_server(self, server_id):
@@ -188,6 +198,9 @@ class TestOpenStackClient(object):
def post_server(self, server):
return self.api_post('/servers', server)['server']
+ def put_server(self, server_id, server):
+ return self.api_put('/servers/%s' % server_id, server)
+
def post_server_action(self, server_id, data):
return self.api_post('/servers/%s/action' % server_id, data)
diff --git a/nova/tests/integrated/test_servers.py b/nova/tests/integrated/test_servers.py
index fcb517cf5..4e8e85c7b 100644
--- a/nova/tests/integrated/test_servers.py
+++ b/nova/tests/integrated/test_servers.py
@@ -285,6 +285,25 @@ class ServersTest(integrated_helpers._IntegratedTestBase):
# Cleanup
self._delete_server(created_server_id)
+ def test_rename_server(self):
+ """Test building and renaming a server."""
+
+ # Create a server
+ server = self._build_minimal_create_server_request()
+ created_server = self.api.post_server({'server': server})
+ LOG.debug("created_server: %s" % created_server)
+ server_id = created_server['id']
+ self.assertTrue(server_id)
+
+ # Rename the server to 'new-name'
+ self.api.put_server(server_id, {'server': {'name': 'new-name'}})
+
+ # Check the name of the server
+ created_server = self.api.get_server(server_id)
+ self.assertEqual(created_server['name'], 'new-name')
+
+ # Cleanup
+ self._delete_server(server_id)
if __name__ == "__main__":
unittest.main()
diff --git a/nova/tests/scheduler/test_zone_aware_scheduler.py b/nova/tests/scheduler/test_zone_aware_scheduler.py
index 5950f4551..d74b71fb6 100644
--- a/nova/tests/scheduler/test_zone_aware_scheduler.py
+++ b/nova/tests/scheduler/test_zone_aware_scheduler.py
@@ -122,19 +122,19 @@ def fake_decrypt_blob_returns_child_info(blob):
def fake_call_zone_method(context, method, specs, zones):
return [
- ('zone1', [
+ (1, [
dict(weight=1, blob='AAAAAAA'),
dict(weight=111, blob='BBBBBBB'),
dict(weight=112, blob='CCCCCCC'),
dict(weight=113, blob='DDDDDDD'),
]),
- ('zone2', [
+ (2, [
dict(weight=120, blob='EEEEEEE'),
dict(weight=2, blob='FFFFFFF'),
dict(weight=122, blob='GGGGGGG'),
dict(weight=123, blob='HHHHHHH'),
]),
- ('zone3', [
+ (3, [
dict(weight=130, blob='IIIIIII'),
dict(weight=131, blob='JJJJJJJ'),
dict(weight=132, blob='KKKKKKK'),
diff --git a/nova/tests/test_api.py b/nova/tests/test_api.py
index 20b20fcbf..26ac5ff24 100644
--- a/nova/tests/test_api.py
+++ b/nova/tests/test_api.py
@@ -92,7 +92,9 @@ class XmlConversionTestCase(test.TestCase):
conv = ec2utils._try_convert
self.assertEqual(conv('None'), None)
self.assertEqual(conv('True'), True)
+ self.assertEqual(conv('true'), True)
self.assertEqual(conv('False'), False)
+ self.assertEqual(conv('false'), False)
self.assertEqual(conv('0'), 0)
self.assertEqual(conv('42'), 42)
self.assertEqual(conv('3.14'), 3.14)
@@ -107,6 +109,8 @@ class Ec2utilsTestCase(test.TestCase):
def test_ec2_id_to_id(self):
self.assertEqual(ec2utils.ec2_id_to_id('i-0000001e'), 30)
self.assertEqual(ec2utils.ec2_id_to_id('ami-1d'), 29)
+ self.assertEqual(ec2utils.ec2_id_to_id('snap-0000001c'), 28)
+ self.assertEqual(ec2utils.ec2_id_to_id('vol-0000001b'), 27)
def test_bad_ec2_id(self):
self.assertRaises(exception.InvalidEc2Id,
@@ -116,6 +120,72 @@ class Ec2utilsTestCase(test.TestCase):
def test_id_to_ec2_id(self):
self.assertEqual(ec2utils.id_to_ec2_id(30), 'i-0000001e')
self.assertEqual(ec2utils.id_to_ec2_id(29, 'ami-%08x'), 'ami-0000001d')
+ self.assertEqual(ec2utils.id_to_ec2_snap_id(28), 'snap-0000001c')
+ self.assertEqual(ec2utils.id_to_ec2_vol_id(27), 'vol-0000001b')
+
+ def test_dict_from_dotted_str(self):
+ in_str = [('BlockDeviceMapping.1.DeviceName', '/dev/sda1'),
+ ('BlockDeviceMapping.1.Ebs.SnapshotId', 'snap-0000001c'),
+ ('BlockDeviceMapping.1.Ebs.VolumeSize', '80'),
+ ('BlockDeviceMapping.1.Ebs.DeleteOnTermination', 'false'),
+ ('BlockDeviceMapping.2.DeviceName', '/dev/sdc'),
+ ('BlockDeviceMapping.2.VirtualName', 'ephemeral0')]
+ expected_dict = {
+ 'block_device_mapping': {
+ '1': {'device_name': '/dev/sda1',
+ 'ebs': {'snapshot_id': 'snap-0000001c',
+ 'volume_size': 80,
+ 'delete_on_termination': False}},
+ '2': {'device_name': '/dev/sdc',
+ 'virtual_name': 'ephemeral0'}}}
+ out_dict = ec2utils.dict_from_dotted_str(in_str)
+
+ self.assertDictMatch(out_dict, expected_dict)
+
+ def test_properties_root_defice_name(self):
+ mappings = [{"device": "/dev/sda1", "virtual": "root"}]
+ properties0 = {'mappings': mappings}
+ properties1 = {'root_device_name': '/dev/sdb', 'mappings': mappings}
+
+ root_device_name = ec2utils.properties_root_device_name(properties0)
+ self.assertEqual(root_device_name, '/dev/sda1')
+
+ root_device_name = ec2utils.properties_root_device_name(properties1)
+ self.assertEqual(root_device_name, '/dev/sdb')
+
+ def test_mapping_prepend_dev(self):
+ mappings = [
+ {'virtual': 'ami',
+ 'device': 'sda1'},
+ {'virtual': 'root',
+ 'device': '/dev/sda1'},
+
+ {'virtual': 'swap',
+ 'device': 'sdb1'},
+ {'virtual': 'swap',
+ 'device': '/dev/sdb2'},
+
+ {'virtual': 'ephemeral0',
+ 'device': 'sdc1'},
+ {'virtual': 'ephemeral1',
+ 'device': '/dev/sdc1'}]
+ expected_result = [
+ {'virtual': 'ami',
+ 'device': 'sda1'},
+ {'virtual': 'root',
+ 'device': '/dev/sda1'},
+
+ {'virtual': 'swap',
+ 'device': '/dev/sdb1'},
+ {'virtual': 'swap',
+ 'device': '/dev/sdb2'},
+
+ {'virtual': 'ephemeral0',
+ 'device': '/dev/sdc1'},
+ {'virtual': 'ephemeral1',
+ 'device': '/dev/sdc1'}]
+ self.assertDictListMatch(ec2utils.mappings_prepend_dev(mappings),
+ expected_result)
class ApiEc2TestCase(test.TestCase):
diff --git a/nova/tests/test_bdm.py b/nova/tests/test_bdm.py
new file mode 100644
index 000000000..b258f6a75
--- /dev/null
+++ b/nova/tests/test_bdm.py
@@ -0,0 +1,233 @@
+# 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 Mapping Code.
+"""
+
+from nova.api.ec2 import cloud
+from nova import test
+
+
+class BlockDeviceMappingEc2CloudTestCase(test.TestCase):
+ """Test Case for Block Device Mapping"""
+
+ def setUp(self):
+ super(BlockDeviceMappingEc2CloudTestCase, self).setUp()
+
+ def tearDown(self):
+ super(BlockDeviceMappingEc2CloudTestCase, self).tearDown()
+
+ def _assertApply(self, action, bdm_list):
+ for bdm, expected_result in bdm_list:
+ self.assertDictMatch(action(bdm), expected_result)
+
+ def test_parse_block_device_mapping(self):
+ bdm_list = [
+ ({'device_name': '/dev/fake0',
+ 'ebs': {'snapshot_id': 'snap-12345678',
+ 'volume_size': 1}},
+ {'device_name': '/dev/fake0',
+ 'snapshot_id': 0x12345678,
+ 'volume_size': 1,
+ 'delete_on_termination': True}),
+
+ ({'device_name': '/dev/fake1',
+ 'ebs': {'snapshot_id': 'snap-23456789',
+ 'delete_on_termination': False}},
+ {'device_name': '/dev/fake1',
+ 'snapshot_id': 0x23456789,
+ 'delete_on_termination': False}),
+
+ ({'device_name': '/dev/fake2',
+ 'ebs': {'snapshot_id': 'vol-87654321',
+ 'volume_size': 2}},
+ {'device_name': '/dev/fake2',
+ 'volume_id': 0x87654321,
+ 'volume_size': 2,
+ 'delete_on_termination': True}),
+
+ ({'device_name': '/dev/fake3',
+ 'ebs': {'snapshot_id': 'vol-98765432',
+ 'delete_on_termination': False}},
+ {'device_name': '/dev/fake3',
+ 'volume_id': 0x98765432,
+ 'delete_on_termination': False}),
+
+ ({'device_name': '/dev/fake4',
+ 'ebs': {'no_device': True}},
+ {'device_name': '/dev/fake4',
+ 'no_device': True}),
+
+ ({'device_name': '/dev/fake5',
+ 'virtual_name': 'ephemeral0'},
+ {'device_name': '/dev/fake5',
+ 'virtual_name': 'ephemeral0'}),
+
+ ({'device_name': '/dev/fake6',
+ 'virtual_name': 'swap'},
+ {'device_name': '/dev/fake6',
+ 'virtual_name': 'swap'}),
+ ]
+ self._assertApply(cloud._parse_block_device_mapping, bdm_list)
+
+ def test_format_block_device_mapping(self):
+ bdm_list = [
+ ({'device_name': '/dev/fake0',
+ 'snapshot_id': 0x12345678,
+ 'volume_size': 1,
+ 'delete_on_termination': True},
+ {'deviceName': '/dev/fake0',
+ 'ebs': {'snapshotId': 'snap-12345678',
+ 'volumeSize': 1,
+ 'deleteOnTermination': True}}),
+
+ ({'device_name': '/dev/fake1',
+ 'snapshot_id': 0x23456789},
+ {'deviceName': '/dev/fake1',
+ 'ebs': {'snapshotId': 'snap-23456789'}}),
+
+ ({'device_name': '/dev/fake2',
+ 'snapshot_id': 0x23456789,
+ 'delete_on_termination': False},
+ {'deviceName': '/dev/fake2',
+ 'ebs': {'snapshotId': 'snap-23456789',
+ 'deleteOnTermination': False}}),
+
+ ({'device_name': '/dev/fake3',
+ 'volume_id': 0x12345678,
+ 'volume_size': 1,
+ 'delete_on_termination': True},
+ {'deviceName': '/dev/fake3',
+ 'ebs': {'snapshotId': 'vol-12345678',
+ 'volumeSize': 1,
+ 'deleteOnTermination': True}}),
+
+ ({'device_name': '/dev/fake4',
+ 'volume_id': 0x23456789},
+ {'deviceName': '/dev/fake4',
+ 'ebs': {'snapshotId': 'vol-23456789'}}),
+
+ ({'device_name': '/dev/fake5',
+ 'volume_id': 0x23456789,
+ 'delete_on_termination': False},
+ {'deviceName': '/dev/fake5',
+ 'ebs': {'snapshotId': 'vol-23456789',
+ 'deleteOnTermination': False}}),
+ ]
+ self._assertApply(cloud._format_block_device_mapping, bdm_list)
+
+ def test_format_mapping(self):
+ properties = {
+ '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': 'ephemeral0',
+ 'device': 'sdc1'},
+ {'virtual': 'ephemeral1',
+ 'device': 'sdc2'},
+ {'virtual': 'ephemeral2',
+ 'device': 'sdc3'},
+ ],
+
+ 'block_device_mapping': [
+ # root
+ {'device_name': '/dev/sda1',
+ 'snapshot_id': 0x12345678,
+ 'delete_on_termination': False},
+
+
+ # overwrite swap
+ {'device_name': '/dev/sdb2',
+ 'snapshot_id': 0x23456789,
+ 'delete_on_termination': False},
+ {'device_name': '/dev/sdb3',
+ 'snapshot_id': 0x3456789A},
+ {'device_name': '/dev/sdb4',
+ 'no_device': True},
+
+ # overwrite ephemeral
+ {'device_name': '/dev/sdc2',
+ 'snapshot_id': 0x3456789A,
+ 'delete_on_termination': False},
+ {'device_name': '/dev/sdc3',
+ 'snapshot_id': 0x456789AB},
+ {'device_name': '/dev/sdc4',
+ 'no_device': True},
+
+ # volume
+ {'device_name': '/dev/sdd1',
+ 'snapshot_id': 0x87654321,
+ 'delete_on_termination': False},
+ {'device_name': '/dev/sdd2',
+ 'snapshot_id': 0x98765432},
+ {'device_name': '/dev/sdd3',
+ 'snapshot_id': 0xA9875463},
+ {'device_name': '/dev/sdd4',
+ 'no_device': True}]}
+
+ expected_result = {
+ 'blockDeviceMapping': [
+ # root
+ {'deviceName': '/dev/sda1',
+ 'ebs': {'snapshotId': 'snap-12345678',
+ 'deleteOnTermination': False}},
+
+ # swap
+ {'deviceName': '/dev/sdb1',
+ 'virtualName': 'swap'},
+ {'deviceName': '/dev/sdb2',
+ 'ebs': {'snapshotId': 'snap-23456789',
+ 'deleteOnTermination': False}},
+ {'deviceName': '/dev/sdb3',
+ 'ebs': {'snapshotId': 'snap-3456789a'}},
+
+ # ephemeral
+ {'deviceName': '/dev/sdc1',
+ 'virtualName': 'ephemeral0'},
+ {'deviceName': '/dev/sdc2',
+ 'ebs': {'snapshotId': 'snap-3456789a',
+ 'deleteOnTermination': False}},
+ {'deviceName': '/dev/sdc3',
+ 'ebs': {'snapshotId': 'snap-456789ab'}},
+
+ # volume
+ {'deviceName': '/dev/sdd1',
+ 'ebs': {'snapshotId': 'snap-87654321',
+ 'deleteOnTermination': False}},
+ {'deviceName': '/dev/sdd2',
+ 'ebs': {'snapshotId': 'snap-98765432'}},
+ {'deviceName': '/dev/sdd3',
+ 'ebs': {'snapshotId': 'snap-a9875463'}}]}
+
+ result = {}
+ cloud._format_mappings(properties, result)
+ print result
+ self.assertEqual(result['blockDeviceMapping'].sort(),
+ expected_result['blockDeviceMapping'].sort())
diff --git a/nova/tests/test_cloud.py b/nova/tests/test_cloud.py
index bf7a2b7ca..a0d50b287 100644
--- a/nova/tests/test_cloud.py
+++ b/nova/tests/test_cloud.py
@@ -45,7 +45,8 @@ LOG = logging.getLogger('nova.tests.cloud')
class CloudTestCase(test.TestCase):
def setUp(self):
super(CloudTestCase, self).setUp()
- self.flags(connection_type='fake')
+ self.flags(connection_type='fake',
+ stub_network=True)
self.conn = rpc.Connection.instance()
@@ -67,7 +68,8 @@ class CloudTestCase(test.TestCase):
host = self.network.host
def fake_show(meh, context, id):
- return {'id': 1, 'properties': {'kernel_id': 1, 'ramdisk_id': 1,
+ return {'id': 1, 'container_format': 'ami',
+ 'properties': {'kernel_id': 1, 'ramdisk_id': 1,
'type': 'machine', 'image_state': 'available'}}
self.stubs.Set(fake._FakeImageService, 'show', fake_show)
@@ -289,7 +291,7 @@ class CloudTestCase(test.TestCase):
vol2 = db.volume_create(self.context, {})
result = self.cloud.describe_volumes(self.context)
self.assertEqual(len(result['volumeSet']), 2)
- volume_id = ec2utils.id_to_ec2_id(vol2['id'], 'vol-%08x')
+ volume_id = ec2utils.id_to_ec2_vol_id(vol2['id'])
result = self.cloud.describe_volumes(self.context,
volume_id=[volume_id])
self.assertEqual(len(result['volumeSet']), 1)
@@ -305,7 +307,7 @@ class CloudTestCase(test.TestCase):
snap = db.snapshot_create(self.context, {'volume_id': vol['id'],
'volume_size': vol['size'],
'status': "available"})
- snapshot_id = ec2utils.id_to_ec2_id(snap['id'], 'snap-%08x')
+ snapshot_id = ec2utils.id_to_ec2_snap_id(snap['id'])
result = self.cloud.create_volume(self.context,
snapshot_id=snapshot_id)
@@ -344,7 +346,7 @@ class CloudTestCase(test.TestCase):
snap2 = db.snapshot_create(self.context, {'volume_id': vol['id']})
result = self.cloud.describe_snapshots(self.context)
self.assertEqual(len(result['snapshotSet']), 2)
- snapshot_id = ec2utils.id_to_ec2_id(snap2['id'], 'snap-%08x')
+ snapshot_id = ec2utils.id_to_ec2_snap_id(snap2['id'])
result = self.cloud.describe_snapshots(self.context,
snapshot_id=[snapshot_id])
self.assertEqual(len(result['snapshotSet']), 1)
@@ -358,7 +360,7 @@ class CloudTestCase(test.TestCase):
def test_create_snapshot(self):
"""Makes sure create_snapshot works."""
vol = db.volume_create(self.context, {'status': "available"})
- volume_id = ec2utils.id_to_ec2_id(vol['id'], 'vol-%08x')
+ volume_id = ec2utils.id_to_ec2_vol_id(vol['id'])
result = self.cloud.create_snapshot(self.context,
volume_id=volume_id)
@@ -375,7 +377,7 @@ class CloudTestCase(test.TestCase):
vol = db.volume_create(self.context, {'status': "available"})
snap = db.snapshot_create(self.context, {'volume_id': vol['id'],
'status': "available"})
- snapshot_id = ec2utils.id_to_ec2_id(snap['id'], 'snap-%08x')
+ snapshot_id = ec2utils.id_to_ec2_snap_id(snap['id'])
result = self.cloud.delete_snapshot(self.context,
snapshot_id=snapshot_id)
@@ -414,11 +416,191 @@ class CloudTestCase(test.TestCase):
db.service_destroy(self.context, comp1['id'])
db.service_destroy(self.context, comp2['id'])
+ def _block_device_mapping_create(self, instance_id, mappings):
+ volumes = []
+ for bdm in mappings:
+ db.block_device_mapping_create(self.context, bdm)
+ if 'volume_id' in bdm:
+ values = {'id': bdm['volume_id']}
+ for bdm_key, vol_key in [('snapshot_id', 'snapshot_id'),
+ ('snapshot_size', 'volume_size'),
+ ('delete_on_termination',
+ 'delete_on_termination')]:
+ if bdm_key in bdm:
+ values[vol_key] = bdm[bdm_key]
+ vol = db.volume_create(self.context, values)
+ db.volume_attached(self.context, vol['id'],
+ instance_id, bdm['device_name'])
+ volumes.append(vol)
+ return volumes
+
+ def _setUpBlockDeviceMapping(self):
+ inst1 = db.instance_create(self.context,
+ {'image_ref': 1,
+ 'root_device_name': '/dev/sdb1'})
+ inst2 = db.instance_create(self.context,
+ {'image_ref': 2,
+ 'root_device_name': '/dev/sdc1'})
+
+ instance_id = inst1['id']
+ mappings0 = [
+ {'instance_id': instance_id,
+ 'device_name': '/dev/sdb1',
+ 'snapshot_id': '1',
+ 'volume_id': '2'},
+ {'instance_id': instance_id,
+ 'device_name': '/dev/sdb2',
+ 'volume_id': '3',
+ 'volume_size': 1},
+ {'instance_id': instance_id,
+ 'device_name': '/dev/sdb3',
+ 'delete_on_termination': True,
+ 'snapshot_id': '4',
+ 'volume_id': '5'},
+ {'instance_id': instance_id,
+ 'device_name': '/dev/sdb4',
+ 'delete_on_termination': False,
+ 'snapshot_id': '6',
+ 'volume_id': '7'},
+ {'instance_id': instance_id,
+ 'device_name': '/dev/sdb5',
+ 'snapshot_id': '8',
+ 'volume_id': '9',
+ 'volume_size': 0},
+ {'instance_id': instance_id,
+ 'device_name': '/dev/sdb6',
+ 'snapshot_id': '10',
+ 'volume_id': '11',
+ 'volume_size': 1},
+ {'instance_id': instance_id,
+ 'device_name': '/dev/sdb7',
+ 'no_device': True},
+ {'instance_id': instance_id,
+ 'device_name': '/dev/sdb8',
+ 'virtual_name': 'swap'},
+ {'instance_id': instance_id,
+ 'device_name': '/dev/sdb9',
+ 'virtual_name': 'ephemeral3'}]
+
+ volumes = self._block_device_mapping_create(instance_id, mappings0)
+ return (inst1, inst2, volumes)
+
+ def _tearDownBlockDeviceMapping(self, inst1, inst2, volumes):
+ for vol in volumes:
+ db.volume_destroy(self.context, vol['id'])
+ for id in (inst1['id'], inst2['id']):
+ for bdm in db.block_device_mapping_get_all_by_instance(
+ self.context, id):
+ db.block_device_mapping_destroy(self.context, bdm['id'])
+ db.instance_destroy(self.context, inst2['id'])
+ db.instance_destroy(self.context, inst1['id'])
+
+ _expected_instance_bdm1 = {
+ 'instanceId': 'i-00000001',
+ 'rootDeviceName': '/dev/sdb1',
+ 'rootDeviceType': 'ebs'}
+
+ _expected_block_device_mapping0 = [
+ {'deviceName': '/dev/sdb1',
+ 'ebs': {'status': 'in-use',
+ 'deleteOnTermination': False,
+ 'volumeId': 2,
+ }},
+ {'deviceName': '/dev/sdb2',
+ 'ebs': {'status': 'in-use',
+ 'deleteOnTermination': False,
+ 'volumeId': 3,
+ }},
+ {'deviceName': '/dev/sdb3',
+ 'ebs': {'status': 'in-use',
+ 'deleteOnTermination': True,
+ 'volumeId': 5,
+ }},
+ {'deviceName': '/dev/sdb4',
+ 'ebs': {'status': 'in-use',
+ 'deleteOnTermination': False,
+ 'volumeId': 7,
+ }},
+ {'deviceName': '/dev/sdb5',
+ 'ebs': {'status': 'in-use',
+ 'deleteOnTermination': False,
+ 'volumeId': 9,
+ }},
+ {'deviceName': '/dev/sdb6',
+ 'ebs': {'status': 'in-use',
+ 'deleteOnTermination': False,
+ 'volumeId': 11, }}]
+ # NOTE(yamahata): swap/ephemeral device case isn't supported yet.
+
+ _expected_instance_bdm2 = {
+ 'instanceId': 'i-00000002',
+ 'rootDeviceName': '/dev/sdc1',
+ 'rootDeviceType': 'instance-store'}
+
+ def test_format_instance_bdm(self):
+ (inst1, inst2, volumes) = self._setUpBlockDeviceMapping()
+
+ result = {}
+ self.cloud._format_instance_bdm(self.context, inst1['id'], '/dev/sdb1',
+ result)
+ self.assertSubDictMatch(
+ {'rootDeviceType': self._expected_instance_bdm1['rootDeviceType']},
+ result)
+ self._assertEqualBlockDeviceMapping(
+ self._expected_block_device_mapping0, result['blockDeviceMapping'])
+
+ result = {}
+ self.cloud._format_instance_bdm(self.context, inst2['id'], '/dev/sdc1',
+ result)
+ self.assertSubDictMatch(
+ {'rootDeviceType': self._expected_instance_bdm2['rootDeviceType']},
+ result)
+
+ self._tearDownBlockDeviceMapping(inst1, inst2, volumes)
+
+ def _assertInstance(self, instance_id):
+ ec2_instance_id = ec2utils.id_to_ec2_id(instance_id)
+ result = self.cloud.describe_instances(self.context,
+ instance_id=[ec2_instance_id])
+ result = result['reservationSet'][0]
+ self.assertEqual(len(result['instancesSet']), 1)
+ result = result['instancesSet'][0]
+ self.assertEqual(result['instanceId'], ec2_instance_id)
+ return result
+
+ def _assertEqualBlockDeviceMapping(self, expected, result):
+ self.assertEqual(len(expected), len(result))
+ for x in expected:
+ found = False
+ for y in result:
+ if x['deviceName'] == y['deviceName']:
+ self.assertSubDictMatch(x, y)
+ found = True
+ break
+ self.assertTrue(found)
+
+ def test_describe_instances_bdm(self):
+ """Make sure describe_instances works with root_device_name and
+ block device mappings
+ """
+ (inst1, inst2, volumes) = self._setUpBlockDeviceMapping()
+
+ result = self._assertInstance(inst1['id'])
+ self.assertSubDictMatch(self._expected_instance_bdm1, result)
+ self._assertEqualBlockDeviceMapping(
+ self._expected_block_device_mapping0, result['blockDeviceMapping'])
+
+ result = self._assertInstance(inst2['id'])
+ self.assertSubDictMatch(self._expected_instance_bdm2, result)
+
+ self._tearDownBlockDeviceMapping(inst1, inst2, volumes)
+
def test_describe_images(self):
describe_images = self.cloud.describe_images
def fake_detail(meh, context):
- return [{'id': 1, 'properties': {'kernel_id': 1, 'ramdisk_id': 1,
+ return [{'id': 1, 'container_format': 'ami',
+ 'properties': {'kernel_id': 1, 'ramdisk_id': 1,
'type': 'machine'}}]
def fake_show_none(meh, context, id):
@@ -443,12 +625,168 @@ class CloudTestCase(test.TestCase):
self.assertRaises(exception.ImageNotFound, describe_images,
self.context, ['ami-fake'])
+ def assertDictListUnorderedMatch(self, L1, L2, key):
+ self.assertEqual(len(L1), len(L2))
+ for d1 in L1:
+ self.assertTrue(key in d1)
+ for d2 in L2:
+ self.assertTrue(key in d2)
+ if d1[key] == d2[key]:
+ self.assertDictMatch(d1, d2)
+
+ def _setUpImageSet(self, create_volumes_and_snapshots=False):
+ mappings1 = [
+ {'device': '/dev/sda1', 'virtual': 'root'},
+
+ {'device': 'sdb0', 'virtual': 'ephemeral0'},
+ {'device': 'sdb1', 'virtual': 'ephemeral1'},
+ {'device': 'sdb2', 'virtual': 'ephemeral2'},
+ {'device': 'sdb3', 'virtual': 'ephemeral3'},
+ {'device': 'sdb4', 'virtual': 'ephemeral4'},
+
+ {'device': 'sdc0', 'virtual': 'swap'},
+ {'device': 'sdc1', 'virtual': 'swap'},
+ {'device': 'sdc2', 'virtual': 'swap'},
+ {'device': 'sdc3', 'virtual': 'swap'},
+ {'device': 'sdc4', 'virtual': 'swap'}]
+ block_device_mapping1 = [
+ {'device_name': '/dev/sdb1', 'snapshot_id': 01234567},
+ {'device_name': '/dev/sdb2', 'volume_id': 01234567},
+ {'device_name': '/dev/sdb3', 'virtual_name': 'ephemeral5'},
+ {'device_name': '/dev/sdb4', 'no_device': True},
+
+ {'device_name': '/dev/sdc1', 'snapshot_id': 12345678},
+ {'device_name': '/dev/sdc2', 'volume_id': 12345678},
+ {'device_name': '/dev/sdc3', 'virtual_name': 'ephemeral6'},
+ {'device_name': '/dev/sdc4', 'no_device': True}]
+ image1 = {
+ 'id': 1,
+ 'properties': {
+ 'kernel_id': 1,
+ 'type': 'machine',
+ 'image_state': 'available',
+ 'mappings': mappings1,
+ 'block_device_mapping': block_device_mapping1,
+ }
+ }
+
+ mappings2 = [{'device': '/dev/sda1', 'virtual': 'root'}]
+ block_device_mapping2 = [{'device_name': '/dev/sdb1',
+ 'snapshot_id': 01234567}]
+ image2 = {
+ 'id': 2,
+ 'properties': {
+ 'kernel_id': 2,
+ 'type': 'machine',
+ 'root_device_name': '/dev/sdb1',
+ 'mappings': mappings2,
+ 'block_device_mapping': block_device_mapping2}}
+
+ def fake_show(meh, context, image_id):
+ for i in [image1, image2]:
+ if i['id'] == image_id:
+ return i
+ raise exception.ImageNotFound(image_id=image_id)
+
+ def fake_detail(meh, context):
+ return [image1, image2]
+
+ self.stubs.Set(fake._FakeImageService, 'show', fake_show)
+ self.stubs.Set(fake._FakeImageService, 'detail', fake_detail)
+
+ volumes = []
+ snapshots = []
+ if create_volumes_and_snapshots:
+ for bdm in block_device_mapping1:
+ if 'volume_id' in bdm:
+ vol = self._volume_create(bdm['volume_id'])
+ volumes.append(vol['id'])
+ if 'snapshot_id' in bdm:
+ snap = db.snapshot_create(self.context,
+ {'id': bdm['snapshot_id'],
+ 'volume_id': 76543210,
+ 'status': "available",
+ 'volume_size': 1})
+ snapshots.append(snap['id'])
+ return (volumes, snapshots)
+
+ def _assertImageSet(self, result, root_device_type, root_device_name):
+ self.assertEqual(1, len(result['imagesSet']))
+ result = result['imagesSet'][0]
+ self.assertTrue('rootDeviceType' in result)
+ self.assertEqual(result['rootDeviceType'], root_device_type)
+ self.assertTrue('rootDeviceName' in result)
+ self.assertEqual(result['rootDeviceName'], root_device_name)
+ self.assertTrue('blockDeviceMapping' in result)
+
+ return result
+
+ _expected_root_device_name1 = '/dev/sda1'
+ # NOTE(yamahata): noDevice doesn't make sense when returning mapping
+ # It makes sense only when user overriding existing
+ # mapping.
+ _expected_bdms1 = [
+ {'deviceName': '/dev/sdb0', 'virtualName': 'ephemeral0'},
+ {'deviceName': '/dev/sdb1', 'ebs': {'snapshotId':
+ 'snap-00053977'}},
+ {'deviceName': '/dev/sdb2', 'ebs': {'snapshotId':
+ 'vol-00053977'}},
+ {'deviceName': '/dev/sdb3', 'virtualName': 'ephemeral5'},
+ # {'deviceName': '/dev/sdb4', 'noDevice': True},
+
+ {'deviceName': '/dev/sdc0', 'virtualName': 'swap'},
+ {'deviceName': '/dev/sdc1', 'ebs': {'snapshotId':
+ 'snap-00bc614e'}},
+ {'deviceName': '/dev/sdc2', 'ebs': {'snapshotId':
+ 'vol-00bc614e'}},
+ {'deviceName': '/dev/sdc3', 'virtualName': 'ephemeral6'},
+ # {'deviceName': '/dev/sdc4', 'noDevice': True}
+ ]
+
+ _expected_root_device_name2 = '/dev/sdb1'
+ _expected_bdms2 = [{'deviceName': '/dev/sdb1',
+ 'ebs': {'snapshotId': 'snap-00053977'}}]
+
+ # NOTE(yamahata):
+ # InstanceBlockDeviceMappingItemType
+ # rootDeviceType
+ # rootDeviceName
+ # blockDeviceMapping
+ # deviceName
+ # virtualName
+ # ebs
+ # snapshotId
+ # volumeSize
+ # deleteOnTermination
+ # noDevice
+ def test_describe_image_mapping(self):
+ """test for rootDeviceName and blockDeiceMapping"""
+ describe_images = self.cloud.describe_images
+ self._setUpImageSet()
+
+ result = describe_images(self.context, ['ami-00000001'])
+ result = self._assertImageSet(result, 'instance-store',
+ self._expected_root_device_name1)
+
+ self.assertDictListUnorderedMatch(result['blockDeviceMapping'],
+ self._expected_bdms1, 'deviceName')
+
+ result = describe_images(self.context, ['ami-00000002'])
+ result = self._assertImageSet(result, 'ebs',
+ self._expected_root_device_name2)
+
+ self.assertDictListUnorderedMatch(result['blockDeviceMapping'],
+ self._expected_bdms2, 'deviceName')
+
+ self.stubs.UnsetAll()
+
def test_describe_image_attribute(self):
describe_image_attribute = self.cloud.describe_image_attribute
def fake_show(meh, context, id):
return {'id': 1, 'properties': {'kernel_id': 1, 'ramdisk_id': 1,
- 'type': 'machine'}, 'is_public': True}
+ 'type': 'machine'}, 'container_format': 'ami',
+ 'is_public': True}
self.stubs.Set(fake._FakeImageService, 'show', fake_show)
self.stubs.Set(fake._FakeImageService, 'show_by_name', fake_show)
@@ -456,11 +794,38 @@ class CloudTestCase(test.TestCase):
'launchPermission')
self.assertEqual([{'group': 'all'}], result['launchPermission'])
+ def test_describe_image_attribute_root_device_name(self):
+ describe_image_attribute = self.cloud.describe_image_attribute
+ self._setUpImageSet()
+
+ result = describe_image_attribute(self.context, 'ami-00000001',
+ 'rootDeviceName')
+ self.assertEqual(result['rootDeviceName'],
+ self._expected_root_device_name1)
+ result = describe_image_attribute(self.context, 'ami-00000002',
+ 'rootDeviceName')
+ self.assertEqual(result['rootDeviceName'],
+ self._expected_root_device_name2)
+
+ def test_describe_image_attribute_block_device_mapping(self):
+ describe_image_attribute = self.cloud.describe_image_attribute
+ self._setUpImageSet()
+
+ result = describe_image_attribute(self.context, 'ami-00000001',
+ 'blockDeviceMapping')
+ self.assertDictListUnorderedMatch(result['blockDeviceMapping'],
+ self._expected_bdms1, 'deviceName')
+ result = describe_image_attribute(self.context, 'ami-00000002',
+ 'blockDeviceMapping')
+ self.assertDictListUnorderedMatch(result['blockDeviceMapping'],
+ self._expected_bdms2, 'deviceName')
+
def test_modify_image_attribute(self):
modify_image_attribute = self.cloud.modify_image_attribute
def fake_show(meh, context, id):
- return {'id': 1, 'properties': {'kernel_id': 1, 'ramdisk_id': 1,
+ return {'id': 1, 'container_format': 'ami',
+ 'properties': {'kernel_id': 1, 'ramdisk_id': 1,
'type': 'machine'}, 'is_public': False}
def fake_update(meh, context, image_id, metadata, data=None):
@@ -494,6 +859,16 @@ class CloudTestCase(test.TestCase):
self.assertRaises(exception.ImageNotFound, deregister_image,
self.context, 'ami-bad001')
+ def test_deregister_image_wrong_container_type(self):
+ deregister_image = self.cloud.deregister_image
+
+ def fake_delete(self, context, id):
+ return None
+
+ self.stubs.Set(fake._FakeImageService, 'delete', fake_delete)
+ self.assertRaises(exception.NotFound, deregister_image, self.context,
+ 'aki-00000001')
+
def _run_instance(self, **kwargs):
rv = self.cloud.run_instances(self.context, **kwargs)
instance_id = rv['instancesSet'][0]['instanceId']
@@ -609,7 +984,7 @@ class CloudTestCase(test.TestCase):
def fake_show_no_state(self, context, id):
return {'id': 1, 'properties': {'kernel_id': 1, 'ramdisk_id': 1,
- 'type': 'machine'}}
+ 'type': 'machine'}, 'container_format': 'ami'}
self.stubs.UnsetAll()
self.stubs.Set(fake._FakeImageService, 'show', fake_show_no_state)
@@ -623,7 +998,8 @@ class CloudTestCase(test.TestCase):
run_instances = self.cloud.run_instances
def fake_show_decrypt(self, context, id):
- return {'id': 1, 'properties': {'kernel_id': 1, 'ramdisk_id': 1,
+ return {'id': 1, 'container_format': 'ami',
+ 'properties': {'kernel_id': 1, 'ramdisk_id': 1,
'type': 'machine', 'image_state': 'decrypting'}}
self.stubs.UnsetAll()
@@ -638,7 +1014,8 @@ class CloudTestCase(test.TestCase):
run_instances = self.cloud.run_instances
def fake_show_stat_active(self, context, id):
- return {'id': 1, 'properties': {'kernel_id': 1, 'ramdisk_id': 1,
+ return {'id': 1, 'container_format': 'ami',
+ 'properties': {'kernel_id': 1, 'ramdisk_id': 1,
'type': 'machine'}, 'status': 'active'}
self.stubs.Set(fake._FakeImageService, 'show', fake_show_stat_active)
@@ -683,7 +1060,7 @@ class CloudTestCase(test.TestCase):
def test_update_of_volume_display_fields(self):
vol = db.volume_create(self.context, {})
self.cloud.update_volume(self.context,
- ec2utils.id_to_ec2_id(vol['id'], 'vol-%08x'),
+ ec2utils.id_to_ec2_vol_id(vol['id']),
display_name='c00l v0lum3')
vol = db.volume_get(self.context, vol['id'])
self.assertEqual('c00l v0lum3', vol['display_name'])
@@ -692,7 +1069,7 @@ class CloudTestCase(test.TestCase):
def test_update_of_volume_wont_update_private_fields(self):
vol = db.volume_create(self.context, {})
self.cloud.update_volume(self.context,
- ec2utils.id_to_ec2_id(vol['id'], 'vol-%08x'),
+ ec2utils.id_to_ec2_vol_id(vol['id']),
mountpoint='/not/here')
vol = db.volume_get(self.context, vol['id'])
self.assertEqual(None, vol['mountpoint'])
@@ -770,11 +1147,13 @@ class CloudTestCase(test.TestCase):
self._restart_compute_service()
- def _volume_create(self):
+ def _volume_create(self, volume_id=None):
kwargs = {'status': 'available',
'host': self.volume.host,
'size': 1,
'attach_status': 'detached', }
+ if volume_id:
+ kwargs['id'] = volume_id
return db.volume_create(self.context, kwargs)
def _assert_volume_attached(self, vol, instance_id, mountpoint):
@@ -803,10 +1182,10 @@ class CloudTestCase(test.TestCase):
'max_count': 1,
'block_device_mapping': [{'device_name': '/dev/vdb',
'volume_id': vol1['id'],
- 'delete_on_termination': False, },
+ 'delete_on_termination': False},
{'device_name': '/dev/vdc',
'volume_id': vol2['id'],
- 'delete_on_termination': True, },
+ 'delete_on_termination': True},
]}
ec2_instance_id = self._run_instance_wait(**kwargs)
instance_id = ec2utils.ec2_id_to_id(ec2_instance_id)
@@ -938,7 +1317,7 @@ class CloudTestCase(test.TestCase):
def test_run_with_snapshot(self):
"""Makes sure run/stop/start instance with snapshot works."""
vol = self._volume_create()
- ec2_volume_id = ec2utils.id_to_ec2_id(vol['id'], 'vol-%08x')
+ ec2_volume_id = ec2utils.id_to_ec2_vol_id(vol['id'])
ec2_snapshot1_id = self._create_snapshot(ec2_volume_id)
snapshot1_id = ec2utils.ec2_id_to_id(ec2_snapshot1_id)
@@ -997,3 +1376,33 @@ class CloudTestCase(test.TestCase):
self.cloud.delete_snapshot(self.context, snapshot_id)
greenthread.sleep(0.3)
db.volume_destroy(self.context, vol['id'])
+
+ def test_create_image(self):
+ """Make sure that CreateImage works"""
+ # enforce periodic tasks run in short time to avoid wait for 60s.
+ self._restart_compute_service(periodic_interval=0.3)
+
+ (volumes, snapshots) = self._setUpImageSet(
+ create_volumes_and_snapshots=True)
+
+ kwargs = {'image_id': 'ami-1',
+ 'instance_type': FLAGS.default_instance_type,
+ 'max_count': 1}
+ ec2_instance_id = self._run_instance_wait(**kwargs)
+
+ # TODO(yamahata): s3._s3_create() can't be tested easily by unit test
+ # as there is no unit test for s3.create()
+ ## result = self.cloud.create_image(self.context, ec2_instance_id,
+ ## no_reboot=True)
+ ## ec2_image_id = result['imageId']
+ ## created_image = self.cloud.describe_images(self.context,
+ ## [ec2_image_id])
+
+ self.cloud.terminate_instances(self.context, [ec2_instance_id])
+ for vol in volumes:
+ db.volume_destroy(self.context, vol)
+ for snap in snapshots:
+ db.snapshot_destroy(self.context, snap)
+ # TODO(yamahata): clean up snapshot created by CreateImage.
+
+ self._restart_compute_service()
diff --git a/nova/tests/test_compute.py b/nova/tests/test_compute.py
index 04bb194d5..2900c594e 100644
--- a/nova/tests/test_compute.py
+++ b/nova/tests/test_compute.py
@@ -818,3 +818,114 @@ class ComputeTestCase(test.TestCase):
LOG.info(_("After force-killing instances: %s"), instances)
self.assertEqual(len(instances), 1)
self.assertEqual(power_state.SHUTOFF, instances[0]['state'])
+
+ @staticmethod
+ def _parse_db_block_device_mapping(bdm_ref):
+ attr_list = ('delete_on_termination', 'device_name', 'no_device',
+ 'virtual_name', 'volume_id', 'volume_size', 'snapshot_id')
+ bdm = {}
+ for attr in attr_list:
+ val = bdm_ref.get(attr, None)
+ if val:
+ bdm[attr] = val
+
+ return bdm
+
+ def test_update_block_device_mapping(self):
+ 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': 'ephemeral0', 'device': 'sdc1'},
+ {'virtual': 'ephemeral1', 'device': 'sdc2'},
+ {'virtual': 'ephemeral2', 'device': 'sdc3'}]
+ block_device_mapping = [
+ # root
+ {'device_name': '/dev/sda1',
+ 'snapshot_id': 0x12345678,
+ 'delete_on_termination': False},
+
+
+ # overwrite swap
+ {'device_name': '/dev/sdb2',
+ 'snapshot_id': 0x23456789,
+ 'delete_on_termination': False},
+ {'device_name': '/dev/sdb3',
+ 'snapshot_id': 0x3456789A},
+ {'device_name': '/dev/sdb4',
+ 'no_device': True},
+
+ # overwrite ephemeral
+ {'device_name': '/dev/sdc2',
+ 'snapshot_id': 0x456789AB,
+ 'delete_on_termination': False},
+ {'device_name': '/dev/sdc3',
+ 'snapshot_id': 0x56789ABC},
+ {'device_name': '/dev/sdc4',
+ 'no_device': True},
+
+ # volume
+ {'device_name': '/dev/sdd1',
+ 'snapshot_id': 0x87654321,
+ 'delete_on_termination': False},
+ {'device_name': '/dev/sdd2',
+ 'snapshot_id': 0x98765432},
+ {'device_name': '/dev/sdd3',
+ 'snapshot_id': 0xA9875463},
+ {'device_name': '/dev/sdd4',
+ 'no_device': True}]
+
+ self.compute_api._update_image_block_device_mapping(
+ self.context, 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': 'ephemeral0', 'device_name': '/dev/sdc1'},
+ {'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)
+ 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'},
+ {'snapshot_id': 0x23456789, 'device_name': '/dev/sdb2'},
+ {'snapshot_id': 0x3456789A, 'device_name': '/dev/sdb3'},
+ {'no_device': True, 'device_name': '/dev/sdb4'},
+
+ {'virtual_name': 'ephemeral0', 'device_name': '/dev/sdc1'},
+ {'snapshot_id': 0x456789AB, 'device_name': '/dev/sdc2'},
+ {'snapshot_id': 0x56789ABC, 'device_name': '/dev/sdc3'},
+ {'no_device': True, 'device_name': '/dev/sdc4'},
+
+ {'snapshot_id': 0x87654321, 'device_name': '/dev/sdd1'},
+ {'snapshot_id': 0x98765432, 'device_name': '/dev/sdd2'},
+ {'snapshot_id': 0xA9875463, 'device_name': '/dev/sdd3'},
+ {'no_device': True, 'device_name': '/dev/sdd4'}]
+ bdms.sort()
+ expected_result.sort()
+ self.assertDictListMatch(bdms, expected_result)
+
+ for bdm in db.block_device_mapping_get_all_by_instance(
+ self.context, instance_id):
+ db.block_device_mapping_destroy(self.context, bdm['id'])
+ self.compute.terminate_instance(self.context, instance_id)
diff --git a/nova/tests/test_exception.py b/nova/tests/test_exception.py
index 4d3b9cc73..cd74f8871 100644
--- a/nova/tests/test_exception.py
+++ b/nova/tests/test_exception.py
@@ -32,3 +32,66 @@ class ApiErrorTestCase(test.TestCase):
self.assertEqual(err.__str__(), 'blah code: fake error')
self.assertEqual(err.code, 'blah code')
self.assertEqual(err.msg, 'fake error')
+
+
+class FakeNotifier(object):
+ """Acts like the nova.notifier.api module."""
+ ERROR = 88
+
+ def __init__(self):
+ self.provided_publisher = None
+ self.provided_event = None
+ self.provided_priority = None
+ self.provided_payload = None
+
+ def notify(self, publisher, event, priority, payload):
+ self.provided_publisher = publisher
+ self.provided_event = event
+ self.provided_priority = priority
+ self.provided_payload = payload
+
+
+def good_function():
+ return 99
+
+
+def bad_function_error():
+ raise exception.Error()
+
+
+def bad_function_exception():
+ raise Exception()
+
+
+class WrapExceptionTestCase(test.TestCase):
+ def test_wrap_exception_good_return(self):
+ wrapped = exception.wrap_exception()
+ self.assertEquals(99, wrapped(good_function)())
+
+ def test_wrap_exception_throws_error(self):
+ wrapped = exception.wrap_exception()
+ self.assertRaises(exception.Error, wrapped(bad_function_error))
+
+ def test_wrap_exception_throws_exception(self):
+ wrapped = exception.wrap_exception()
+ # Note that Exception is converted to Error ...
+ self.assertRaises(exception.Error, wrapped(bad_function_exception))
+
+ def test_wrap_exception_with_notifier(self):
+ notifier = FakeNotifier()
+ wrapped = exception.wrap_exception(notifier, "publisher", "event",
+ "level")
+ self.assertRaises(exception.Error, wrapped(bad_function_exception))
+ self.assertEquals(notifier.provided_publisher, "publisher")
+ self.assertEquals(notifier.provided_event, "event")
+ self.assertEquals(notifier.provided_priority, "level")
+ for key in ['exception', 'args']:
+ self.assertTrue(key in notifier.provided_payload.keys())
+
+ def test_wrap_exception_with_notifier_defaults(self):
+ notifier = FakeNotifier()
+ wrapped = exception.wrap_exception(notifier)
+ self.assertRaises(exception.Error, wrapped(bad_function_exception))
+ self.assertEquals(notifier.provided_publisher, None)
+ self.assertEquals(notifier.provided_event, "bad_function_exception")
+ self.assertEquals(notifier.provided_priority, notifier.ERROR)
diff --git a/nova/tests/test_metadata.py b/nova/tests/test_metadata.py
new file mode 100644
index 000000000..c862726ab
--- /dev/null
+++ b/nova/tests/test_metadata.py
@@ -0,0 +1,76 @@
+# 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.
+
+"""Tests for the testing the metadata code."""
+
+import base64
+import httplib
+
+import webob
+
+from nova import test
+from nova import wsgi
+from nova.api.ec2 import metadatarequesthandler
+from nova.db.sqlalchemy import api
+
+
+class MetadataTestCase(test.TestCase):
+ """Test that metadata is returning proper values."""
+
+ def setUp(self):
+ super(MetadataTestCase, self).setUp()
+ self.instance = ({'id': 1,
+ 'project_id': 'test',
+ 'key_name': None,
+ 'host': 'test',
+ 'launch_index': 1,
+ 'instance_type': 'm1.tiny',
+ 'reservation_id': 'r-xxxxxxxx',
+ 'user_data': '',
+ 'image_ref': 7,
+ 'hostname': 'test'})
+
+ def instance_get(*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_floating_address', floating_get)
+ self.app = metadatarequesthandler.MetadataRequestHandler()
+
+ def request(self, relative_url):
+ request = webob.Request.blank(relative_url)
+ request.remote_addr = "127.0.0.1"
+ return request.get_response(self.app).body
+
+ def test_base(self):
+ self.assertEqual(self.request('/'), 'meta-data/\nuser-data')
+
+ def test_user_data(self):
+ self.instance['user_data'] = base64.b64encode('happy')
+ self.assertEqual(self.request('/user-data'), 'happy')
+
+ def test_security_groups(self):
+ def sg_get(*args, **kwargs):
+ return [{'name': 'default'}, {'name': 'other'}]
+ self.stubs.Set(api, 'security_group_get_by_instance', sg_get)
+ self.assertEqual(self.request('/meta-data/security-groups'),
+ 'default\nother')
diff --git a/nova/tests/test_network.py b/nova/tests/test_network.py
index 1740d4f54..7cc174842 100644
--- a/nova/tests/test_network.py
+++ b/nova/tests/test_network.py
@@ -16,6 +16,7 @@
# under the License.
from nova import db
+from nova import exception
from nova import flags
from nova import log as logging
from nova import test
@@ -227,3 +228,35 @@ class VlanNetworkTestCase(test.TestCase):
self.assertRaises(ValueError, self.network.create_networks, None,
num_networks=100, vlan_start=1,
cidr='192.168.0.1/24', network_size=100)
+
+
+class CommonNetworkTestCase(test.TestCase):
+
+ class FakeNetworkManager(network_manager.NetworkManager):
+ """This NetworkManager doesn't call the base class so we can bypass all
+ inherited service cruft and just perform unit tests.
+ """
+
+ class FakeDB:
+ def fixed_ip_get_by_instance(self, context, instance_id):
+ return [dict(address='10.0.0.0'), dict(address='10.0.0.1'),
+ dict(address='10.0.0.2')]
+
+ def __init__(self):
+ self.db = self.FakeDB()
+ self.deallocate_called = None
+
+ def deallocate_fixed_ip(self, context, address):
+ self.deallocate_called = address
+
+ def test_remove_fixed_ip_from_instance(self):
+ manager = self.FakeNetworkManager()
+ manager.remove_fixed_ip_from_instance(None, 99, '10.0.0.1')
+
+ self.assertEquals(manager.deallocate_called, '10.0.0.1')
+
+ def test_remove_fixed_ip_from_instance_bad_input(self):
+ manager = self.FakeNetworkManager()
+ self.assertRaises(exception.FixedIpNotFoundForSpecificInstance,
+ manager.remove_fixed_ip_from_instance,
+ None, 99, 'bad input')
diff --git a/nova/tests/test_volume.py b/nova/tests/test_volume.py
index 62cc4b325..c0f89601f 100644
--- a/nova/tests/test_volume.py
+++ b/nova/tests/test_volume.py
@@ -27,8 +27,10 @@ from nova import exception
from nova import db
from nova import flags
from nova import log as logging
+from nova import rpc
from nova import test
from nova import utils
+from nova import volume
FLAGS = flags.FLAGS
LOG = logging.getLogger('nova.tests.volume')
@@ -43,6 +45,11 @@ class VolumeTestCase(test.TestCase):
self.flags(connection_type='fake')
self.volume = utils.import_object(FLAGS.volume_manager)
self.context = context.get_admin_context()
+ self.instance_id = db.instance_create(self.context, {})['id']
+
+ def tearDown(self):
+ db.instance_destroy(self.context, self.instance_id)
+ super(VolumeTestCase, self).tearDown()
@staticmethod
def _create_volume(size='0', snapshot_id=None):
@@ -223,6 +230,30 @@ class VolumeTestCase(test.TestCase):
snapshot_id)
self.volume.delete_volume(self.context, volume_id)
+ def test_create_snapshot_force(self):
+ """Test snapshot in use can be created forcibly."""
+
+ def fake_cast(ctxt, topic, msg):
+ pass
+ self.stubs.Set(rpc, 'cast', fake_cast)
+
+ volume_id = self._create_volume()
+ self.volume.create_volume(self.context, volume_id)
+ db.volume_attached(self.context, volume_id, self.instance_id,
+ '/dev/sda1')
+
+ volume_api = volume.api.API()
+ self.assertRaises(exception.ApiError,
+ volume_api.create_snapshot,
+ self.context, volume_id,
+ 'fake_name', 'fake_description')
+ snapshot_ref = volume_api.create_snapshot_force(self.context,
+ volume_id,
+ 'fake_name',
+ 'fake_description')
+ db.snapshot_destroy(self.context, snapshot_ref['id'])
+ db.volume_destroy(self.context, volume_id)
+
class DriverTestCase(test.TestCase):
"""Base Test class for Drivers."""
diff --git a/nova/tests/test_zones.py b/nova/tests/test_zones.py
index e132809dc..a943fee27 100644
--- a/nova/tests/test_zones.py
+++ b/nova/tests/test_zones.py
@@ -198,3 +198,178 @@ class ZoneManagerTestCase(test.TestCase):
self.assertEquals(zone_state.attempt, 3)
self.assertFalse(zone_state.is_active)
self.assertEquals(zone_state.name, None)
+
+ def test_host_service_caps_stale_no_stale_service(self):
+ zm = zone_manager.ZoneManager()
+
+ # services just updated capabilities
+ zm.update_service_capabilities("svc1", "host1", dict(a=1, b=2))
+ zm.update_service_capabilities("svc2", "host1", dict(a=3, b=4))
+ self.assertFalse(zm.host_service_caps_stale("host1", "svc1"))
+ self.assertFalse(zm.host_service_caps_stale("host1", "svc2"))
+
+ def test_host_service_caps_stale_all_stale_services(self):
+ zm = zone_manager.ZoneManager()
+ expiry_time = (FLAGS.periodic_interval * 3) + 1
+
+ # Both services became stale
+ zm.update_service_capabilities("svc1", "host1", dict(a=1, b=2))
+ zm.update_service_capabilities("svc2", "host1", dict(a=3, b=4))
+ time_future = utils.utcnow() + datetime.timedelta(seconds=expiry_time)
+ utils.set_time_override(time_future)
+ self.assertTrue(zm.host_service_caps_stale("host1", "svc1"))
+ self.assertTrue(zm.host_service_caps_stale("host1", "svc2"))
+ utils.clear_time_override()
+
+ def test_host_service_caps_stale_one_stale_service(self):
+ zm = zone_manager.ZoneManager()
+ expiry_time = (FLAGS.periodic_interval * 3) + 1
+
+ # One service became stale
+ zm.update_service_capabilities("svc1", "host1", dict(a=1, b=2))
+ zm.update_service_capabilities("svc2", "host1", dict(a=3, b=4))
+ caps = zm.service_states["host1"]["svc1"]
+ caps["timestamp"] = utils.utcnow() - \
+ datetime.timedelta(seconds=expiry_time)
+ self.assertTrue(zm.host_service_caps_stale("host1", "svc1"))
+ self.assertFalse(zm.host_service_caps_stale("host1", "svc2"))
+
+ def test_delete_expired_host_services_del_one_service(self):
+ zm = zone_manager.ZoneManager()
+
+ # Delete one service in a host
+ zm.update_service_capabilities("svc1", "host1", dict(a=1, b=2))
+ zm.update_service_capabilities("svc2", "host1", dict(a=3, b=4))
+ stale_host_services = {"host1": ["svc1"]}
+ zm.delete_expired_host_services(stale_host_services)
+ self.assertFalse("svc1" in zm.service_states["host1"])
+ self.assertTrue("svc2" in zm.service_states["host1"])
+
+ def test_delete_expired_host_services_del_all_hosts(self):
+ zm = zone_manager.ZoneManager()
+
+ # Delete all services in a host
+ zm.update_service_capabilities("svc2", "host1", dict(a=3, b=4))
+ zm.update_service_capabilities("svc1", "host1", dict(a=1, b=2))
+ stale_host_services = {"host1": ["svc1", "svc2"]}
+ zm.delete_expired_host_services(stale_host_services)
+ self.assertFalse("host1" in zm.service_states)
+
+ def test_delete_expired_host_services_del_one_service_per_host(self):
+ zm = zone_manager.ZoneManager()
+
+ # Delete one service per host
+ zm.update_service_capabilities("svc1", "host1", dict(a=1, b=2))
+ zm.update_service_capabilities("svc1", "host2", dict(a=3, b=4))
+ stale_host_services = {"host1": ["svc1"], "host2": ["svc1"]}
+ zm.delete_expired_host_services(stale_host_services)
+ self.assertFalse("host1" in zm.service_states)
+ self.assertFalse("host2" in zm.service_states)
+
+ def test_get_zone_capabilities_one_host(self):
+ zm = zone_manager.ZoneManager()
+
+ # Service capabilities recent
+ zm.update_service_capabilities("svc1", "host1", dict(a=1, b=2))
+ caps = zm.get_zone_capabilities(None)
+ self.assertEquals(caps, dict(svc1_a=(1, 1), svc1_b=(2, 2)))
+
+ def test_get_zone_capabilities_expired_host(self):
+ zm = zone_manager.ZoneManager()
+ expiry_time = (FLAGS.periodic_interval * 3) + 1
+
+ # Service capabilities stale
+ zm.update_service_capabilities("svc1", "host1", dict(a=1, b=2))
+ time_future = utils.utcnow() + datetime.timedelta(seconds=expiry_time)
+ utils.set_time_override(time_future)
+ caps = zm.get_zone_capabilities(None)
+ self.assertEquals(caps, {})
+ utils.clear_time_override()
+
+ def test_get_zone_capabilities_multiple_hosts(self):
+ zm = zone_manager.ZoneManager()
+
+ # Both host service capabilities recent
+ zm.update_service_capabilities("svc1", "host1", dict(a=1, b=2))
+ zm.update_service_capabilities("svc1", "host2", dict(a=3, b=4))
+ caps = zm.get_zone_capabilities(None)
+ self.assertEquals(caps, dict(svc1_a=(1, 3), svc1_b=(2, 4)))
+
+ def test_get_zone_capabilities_one_stale_host(self):
+ zm = zone_manager.ZoneManager()
+ expiry_time = (FLAGS.periodic_interval * 3) + 1
+
+ # One host service capabilities become stale
+ zm.update_service_capabilities("svc1", "host1", dict(a=1, b=2))
+ zm.update_service_capabilities("svc1", "host2", dict(a=3, b=4))
+ serv_caps = zm.service_states["host1"]["svc1"]
+ serv_caps["timestamp"] = utils.utcnow() - \
+ datetime.timedelta(seconds=expiry_time)
+ caps = zm.get_zone_capabilities(None)
+ self.assertEquals(caps, dict(svc1_a=(3, 3), svc1_b=(4, 4)))
+
+ def test_get_zone_capabilities_multiple_service_per_host(self):
+ zm = zone_manager.ZoneManager()
+
+ # Multiple services per host
+ zm.update_service_capabilities("svc1", "host1", dict(a=1, b=2))
+ zm.update_service_capabilities("svc1", "host2", dict(a=3, b=4))
+ zm.update_service_capabilities("svc2", "host1", dict(a=5, b=6))
+ zm.update_service_capabilities("svc2", "host2", dict(a=7, b=8))
+ caps = zm.get_zone_capabilities(None)
+ self.assertEquals(caps, dict(svc1_a=(1, 3), svc1_b=(2, 4),
+ svc2_a=(5, 7), svc2_b=(6, 8)))
+
+ def test_get_zone_capabilities_one_stale_service_per_host(self):
+ zm = zone_manager.ZoneManager()
+ expiry_time = (FLAGS.periodic_interval * 3) + 1
+
+ # Two host services among four become stale
+ zm.update_service_capabilities("svc1", "host1", dict(a=1, b=2))
+ zm.update_service_capabilities("svc1", "host2", dict(a=3, b=4))
+ zm.update_service_capabilities("svc2", "host1", dict(a=5, b=6))
+ zm.update_service_capabilities("svc2", "host2", dict(a=7, b=8))
+ serv_caps_1 = zm.service_states["host1"]["svc2"]
+ serv_caps_1["timestamp"] = utils.utcnow() - \
+ datetime.timedelta(seconds=expiry_time)
+ serv_caps_2 = zm.service_states["host2"]["svc1"]
+ serv_caps_2["timestamp"] = utils.utcnow() - \
+ datetime.timedelta(seconds=expiry_time)
+ caps = zm.get_zone_capabilities(None)
+ self.assertEquals(caps, dict(svc1_a=(1, 1), svc1_b=(2, 2),
+ svc2_a=(7, 7), svc2_b=(8, 8)))
+
+ def test_get_zone_capabilities_three_stale_host_services(self):
+ zm = zone_manager.ZoneManager()
+ expiry_time = (FLAGS.periodic_interval * 3) + 1
+
+ # Three host services among four become stale
+ zm.update_service_capabilities("svc1", "host1", dict(a=1, b=2))
+ zm.update_service_capabilities("svc1", "host2", dict(a=3, b=4))
+ zm.update_service_capabilities("svc2", "host1", dict(a=5, b=6))
+ zm.update_service_capabilities("svc2", "host2", dict(a=7, b=8))
+ serv_caps_1 = zm.service_states["host1"]["svc2"]
+ serv_caps_1["timestamp"] = utils.utcnow() - \
+ datetime.timedelta(seconds=expiry_time)
+ serv_caps_2 = zm.service_states["host2"]["svc1"]
+ serv_caps_2["timestamp"] = utils.utcnow() - \
+ datetime.timedelta(seconds=expiry_time)
+ serv_caps_3 = zm.service_states["host2"]["svc2"]
+ serv_caps_3["timestamp"] = utils.utcnow() - \
+ datetime.timedelta(seconds=expiry_time)
+ caps = zm.get_zone_capabilities(None)
+ self.assertEquals(caps, dict(svc1_a=(1, 1), svc1_b=(2, 2)))
+
+ def test_get_zone_capabilities_all_stale_host_services(self):
+ zm = zone_manager.ZoneManager()
+ expiry_time = (FLAGS.periodic_interval * 3) + 1
+
+ # All the host services become stale
+ zm.update_service_capabilities("svc1", "host1", dict(a=1, b=2))
+ zm.update_service_capabilities("svc1", "host2", dict(a=3, b=4))
+ zm.update_service_capabilities("svc2", "host1", dict(a=5, b=6))
+ zm.update_service_capabilities("svc2", "host2", dict(a=7, b=8))
+ time_future = utils.utcnow() + datetime.timedelta(seconds=expiry_time)
+ utils.set_time_override(time_future)
+ caps = zm.get_zone_capabilities(None)
+ self.assertEquals(caps, {})
diff --git a/nova/virt/driver.py b/nova/virt/driver.py
index 3c4a073bf..178279d31 100644
--- a/nova/virt/driver.py
+++ b/nova/virt/driver.py
@@ -197,7 +197,7 @@ class ComputeDriver(object):
def reset_network(self, instance):
"""reset networking for specified instance"""
- raise NotImplementedError()
+ pass
def ensure_filtering_rules_for_instance(self, instance_ref):
"""Setting up filtering rules and waiting for its completion.
@@ -244,7 +244,7 @@ class ComputeDriver(object):
def inject_network_info(self, instance, nw_info):
"""inject network info for specified instance"""
- raise NotImplementedError()
+ pass
def poll_rescued_instances(self, timeout):
"""Poll for rescued instances"""
diff --git a/nova/virt/libvirt/connection.py b/nova/virt/libvirt/connection.py
index 6bf2cf2b8..edabea46d 100644
--- a/nova/virt/libvirt/connection.py
+++ b/nova/virt/libvirt/connection.py
@@ -331,7 +331,7 @@ class LibvirtConnection(driver.ComputeDriver):
if os.path.exists(target):
shutil.rmtree(target)
- @exception.wrap_exception
+ @exception.wrap_exception()
def attach_volume(self, instance_name, device_path, mountpoint):
virt_dom = self._lookup_by_name(instance_name)
mount_device = mountpoint.rpartition("/")[2]
@@ -375,7 +375,7 @@ class LibvirtConnection(driver.ComputeDriver):
if doc is not None:
doc.freeDoc()
- @exception.wrap_exception
+ @exception.wrap_exception()
def detach_volume(self, instance_name, mountpoint):
virt_dom = self._lookup_by_name(instance_name)
mount_device = mountpoint.rpartition("/")[2]
@@ -384,7 +384,7 @@ class LibvirtConnection(driver.ComputeDriver):
raise exception.DiskNotFound(location=mount_device)
virt_dom.detachDevice(xml)
- @exception.wrap_exception
+ @exception.wrap_exception()
def snapshot(self, instance, image_href):
"""Create snapshot from a running VM instance.
@@ -460,7 +460,7 @@ class LibvirtConnection(driver.ComputeDriver):
# Clean up
shutil.rmtree(temp_dir)
- @exception.wrap_exception
+ @exception.wrap_exception()
def reboot(self, instance):
"""Reboot a virtual machine, given an instance reference.
@@ -501,31 +501,31 @@ class LibvirtConnection(driver.ComputeDriver):
timer = utils.LoopingCall(_wait_for_reboot)
return timer.start(interval=0.5, now=True)
- @exception.wrap_exception
+ @exception.wrap_exception()
def pause(self, instance, callback):
"""Pause VM instance"""
dom = self._lookup_by_name(instance.name)
dom.suspend()
- @exception.wrap_exception
+ @exception.wrap_exception()
def unpause(self, instance, callback):
"""Unpause paused VM instance"""
dom = self._lookup_by_name(instance.name)
dom.resume()
- @exception.wrap_exception
+ @exception.wrap_exception()
def suspend(self, instance, callback):
"""Suspend the specified instance"""
dom = self._lookup_by_name(instance.name)
dom.managedSave(0)
- @exception.wrap_exception
+ @exception.wrap_exception()
def resume(self, instance, callback):
"""resume the specified instance"""
dom = self._lookup_by_name(instance.name)
dom.create()
- @exception.wrap_exception
+ @exception.wrap_exception()
def rescue(self, instance):
"""Loads a VM using rescue images.
@@ -563,7 +563,7 @@ class LibvirtConnection(driver.ComputeDriver):
timer = utils.LoopingCall(_wait_for_rescue)
return timer.start(interval=0.5, now=True)
- @exception.wrap_exception
+ @exception.wrap_exception()
def unrescue(self, instance):
"""Reboot the VM which is being rescued back into primary images.
@@ -573,13 +573,13 @@ class LibvirtConnection(driver.ComputeDriver):
"""
self.reboot(instance)
- @exception.wrap_exception
+ @exception.wrap_exception()
def poll_rescued_instances(self, timeout):
pass
# NOTE(ilyaalekseyev): Implementation like in multinics
# for xenapi(tr3buchet)
- @exception.wrap_exception
+ @exception.wrap_exception()
def spawn(self, instance, network_info=None, block_device_mapping=None):
xml = self.to_xml(instance, False, network_info=network_info,
block_device_mapping=block_device_mapping)
@@ -642,7 +642,7 @@ class LibvirtConnection(driver.ComputeDriver):
LOG.info(_('Contents of file %(fpath)s: %(contents)r') % locals())
return contents
- @exception.wrap_exception
+ @exception.wrap_exception()
def get_console_output(self, instance):
console_log = os.path.join(FLAGS.instances_path, instance['name'],
'console.log')
@@ -663,7 +663,7 @@ class LibvirtConnection(driver.ComputeDriver):
return self._dump_file(fpath)
- @exception.wrap_exception
+ @exception.wrap_exception()
def get_ajax_console(self, instance):
def get_open_port():
start_port, end_port = FLAGS.ajaxterm_portrange.split("-")
@@ -704,7 +704,7 @@ class LibvirtConnection(driver.ComputeDriver):
def get_host_ip_addr(self):
return FLAGS.my_ip
- @exception.wrap_exception
+ @exception.wrap_exception()
def get_vnc_console(self, instance):
def get_vnc_port_for_instance(instance_name):
virt_dom = self._lookup_by_name(instance_name)
diff --git a/nova/virt/xenapi/vmops.py b/nova/virt/xenapi/vmops.py
index 56718f8e8..c332c27b0 100644
--- a/nova/virt/xenapi/vmops.py
+++ b/nova/virt/xenapi/vmops.py
@@ -597,7 +597,9 @@ class VMOps(object):
# No response from the agent
return
resp_dict = json.loads(resp)
- return resp_dict['message']
+ # 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', '')
if timeout:
vm_ref = self._get_vm_opaque_ref(instance)
@@ -662,9 +664,13 @@ class VMOps(object):
# There was some sort of error; the message will contain
# a description of the error.
raise RuntimeError(resp_dict['message'])
- agent_pub = int(resp_dict['message'])
+ # 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', ''))
dh.compute_shared(agent_pub)
- enc_pass = dh.encrypt(new_pass)
+ # Some old versions of Linux and Windows agent expect trailing \n
+ # on password to work correctly.
+ enc_pass = dh.encrypt(new_pass + '\n')
# Send the encrypted password
password_transaction_id = str(uuid.uuid4())
password_args = {'id': password_transaction_id, 'enc_pass': enc_pass}
diff --git a/nova/volume/api.py b/nova/volume/api.py
index 7d27abff9..cfc274c77 100644
--- a/nova/volume/api.py
+++ b/nova/volume/api.py
@@ -140,9 +140,10 @@ class API(base.Base):
{"method": "remove_volume",
"args": {'volume_id': volume_id}})
- def create_snapshot(self, context, volume_id, name, description):
+ def _create_snapshot(self, context, volume_id, name, description,
+ force=False):
volume = self.get(context, volume_id)
- if volume['status'] != "available":
+ if ((not force) and (volume['status'] != "available")):
raise exception.ApiError(_("Volume status must be available"))
options = {
@@ -164,6 +165,14 @@ class API(base.Base):
"snapshot_id": snapshot['id']}})
return snapshot
+ def create_snapshot(self, context, volume_id, name, description):
+ return self._create_snapshot(context, volume_id, name, description,
+ False)
+
+ def create_snapshot_force(self, context, volume_id, name, description):
+ return self._create_snapshot(context, volume_id, name, description,
+ True)
+
def delete_snapshot(self, context, snapshot_id):
snapshot = self.get_snapshot(context, snapshot_id)
if snapshot['status'] != "available":
diff --git a/plugins/xenserver/xenapi/contrib/rpmbuild/SPECS/openstack-xen-plugins.spec b/plugins/xenserver/xenapi/contrib/rpmbuild/SPECS/openstack-xen-plugins.spec
index 91ff20e5f..cb2af2109 100644
--- a/plugins/xenserver/xenapi/contrib/rpmbuild/SPECS/openstack-xen-plugins.spec
+++ b/plugins/xenserver/xenapi/contrib/rpmbuild/SPECS/openstack-xen-plugins.spec
@@ -5,6 +5,7 @@ Summary: Files for XenAPI support.
License: ASL 2.0
Group: Applications/Utilities
Source0: openstack-xen-plugins.tar.gz
+BuildArch: noarch
BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-root-%(%{__id_u} -n)
Requires: parted
diff --git a/plugins/xenserver/xenapi/etc/xapi.d/plugins/agent b/plugins/xenserver/xenapi/etc/xapi.d/plugins/agent
index b8a1b936a..288ccc78a 100755
--- a/plugins/xenserver/xenapi/etc/xapi.d/plugins/agent
+++ b/plugins/xenserver/xenapi/etc/xapi.d/plugins/agent
@@ -37,7 +37,7 @@ import time
import XenAPIPlugin
from pluginlib_nova import *
-configure_logging("xenstore")
+configure_logging("agent")
import xenstore
AGENT_TIMEOUT = 30
@@ -114,7 +114,6 @@ def resetnetwork(self, arg_dict):
xenstore.write_record(self, arg_dict)
-@jsonify
def inject_file(self, arg_dict):
"""Expects a file path and the contents of the file to be written. Both
should be base64-encoded in order to eliminate errors as they are passed
@@ -127,20 +126,21 @@ def inject_file(self, arg_dict):
been disabled, and raise a NotImplemented error if that is the case.
"""
b64_path = arg_dict["b64_path"]
- b64_file = arg_dict["b64_file"]
+ b64_file = arg_dict["b64_contents"]
request_id = arg_dict["id"]
- if self._agent_has_method("file_inject"):
+ agent_features = _get_agent_features(self, arg_dict)
+ if "file_inject" in agent_features:
# New version of the agent. Agent should receive a 'value'
# key whose value is a dictionary containing 'b64_path' and
# 'b64_file'. See old version below.
arg_dict["value"] = json.dumps({"name": "file_inject",
"value": {"b64_path": b64_path, "b64_file": b64_file}})
- elif self._agent_has_method("injectfile"):
+ elif "injectfile" in agent_features:
# Old agent requires file path and file contents to be
# combined into one base64 value.
raw_path = base64.b64decode(b64_path)
raw_file = base64.b64decode(b64_file)
- new_b64 = base64.b64encode("%s,%s") % (raw_path, raw_file)
+ new_b64 = base64.b64encode("%s,%s" % (raw_path, raw_file))
arg_dict["value"] = json.dumps({"name": "injectfile",
"value": new_b64})
else:
@@ -174,30 +174,23 @@ def agent_update(self, arg_dict):
return resp
-def _agent_has_method(self, method):
- """Check that the agent has a particular method by checking its
- features. Cache the features so we don't have to query the agent
- every time we need to check.
- """
+def _get_agent_features(self, arg_dict):
+ """Return an array of features that an agent supports."""
+ tmp_id = commands.getoutput("uuidgen")
+ dct = {}
+ dct.update(arg_dict)
+ dct["value"] = json.dumps({"name": "features", "value": ""})
+ dct["path"] = "data/host/%s" % tmp_id
+ xenstore.write_record(self, dct)
try:
- self._agent_methods
- except AttributeError:
- self._agent_methods = []
- if not self._agent_methods:
- # Haven't been defined
- tmp_id = commands.getoutput("uuidgen")
- dct = {}
- dct["value"] = json.dumps({"name": "features", "value": ""})
- dct["path"] = "data/host/%s" % tmp_id
- xenstore.write_record(self, dct)
- try:
- resp = _wait_for_agent(self, tmp_id, dct)
- except TimeoutError, e:
- raise PluginError(e)
- response = json.loads(resp)
- # The agent returns a comma-separated list of methods.
- self._agent_methods = response.split(",")
- return method in self._agent_methods
+ resp = _wait_for_agent(self, tmp_id, dct)
+ except TimeoutError, e:
+ raise PluginError(e)
+ response = json.loads(resp)
+ if response['returncode'] != 0:
+ return response["message"].split(",")
+ else:
+ return {}
def _wait_for_agent(self, request_id, arg_dict):
diff --git a/tools/clean-vlans b/tools/clean-vlans
index 820a9dbe5..a26ad86ad 100755
--- a/tools/clean-vlans
+++ b/tools/clean-vlans
@@ -17,9 +17,9 @@
# License for the specific language governing permissions and limitations
# under the License.
-export LC_ALL=C
+export LC_ALL=C
sudo ifconfig -a | grep br | grep -v bridge | cut -f1 -d" " | xargs -n1 -ifoo ifconfig foo down
sudo ifconfig -a | grep br | grep -v bridge | cut -f1 -d" " | xargs -n1 -ifoo brctl delbr foo
-sudo ifconfig -a | grep vlan | grep -v vlan124 | grep -v vlan5 | cut -f1 -d" " | xargs -n1 -ifoo ifconfig foo down
-sudo ifconfig -a | grep vlan | grep -v vlan124 | grep -v vlan5 | cut -f1 -d" " | xargs -n1 -ifoo vconfig rem foo
+sudo ifconfig -a | grep vlan | cut -f1 -d" " | xargs -n1 -ifoo ifconfig foo down
+sudo ifconfig -a | grep vlan | cut -f1 -d" " | xargs -n1 -ifoo vconfig rem foo
diff --git a/tools/nova-debug b/tools/nova-debug
index 3ff68ca35..0a78af16a 100755
--- a/tools/nova-debug
+++ b/tools/nova-debug
@@ -30,13 +30,15 @@ cd $INSTANCES_PATH/$1
if [ $CMD != "umount" ] && [ $CMD != "launch" ]; then
# destroy the instance
virsh destroy $1
+virsh undefine $1
# mount the filesystem
mkdir t
-DEVICE=`losetup --show -f disk`
+DEVICE=/dev/nbd0
echo $DEVICE
-kpartx -a $DEVICE
-mount /dev/mapper/${DEVICE:4}p1 t
+qemu-nbd -c $DEVICE disk
+sleep 3
+mount $DEVICE t
fi
if [ $CMD != "mount" ] && [ $CMD != "umount" ]; then
@@ -66,11 +68,13 @@ sed -i "s/<serial type=\"file\">.*<\/serial>/<serial type=\"pty\"><source path=\
umount t
-virsh create debug.xml
+virsh define debug.xml
+virsh start $1
virsh console $1
virsh destroy $1
+virsh undefine $1
-mount /dev/mapper/${DEVICE:4}p1 t
+mount $DEVICE t
# clear debug root password
chroot t passwd -l root
@@ -83,10 +87,11 @@ if [ $CMD != "mount" ] && [ $CMD != "launch" ]; then
# unmount the filesystem
umount t
-kpartx -d $DEVICE
-losetup -d $DEVICE
+qemu-nbd -d $DEVICE
rmdir t
# recreate the instance
-virsh create libvirt.xml
+virsh define libvirt.xml
+virsh start $1
fi
+