From d764a483497afc5d029a82db14cc5cc88f45f4c0 Mon Sep 17 00:00:00 2001 From: "Kevin L. Mitchell" Date: Thu, 9 Jun 2011 19:43:48 +0000 Subject: Add an extension to allow for an addFixedIp action on instances --- nova/api/openstack/contrib/multinic.py | 83 ++++++++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 nova/api/openstack/contrib/multinic.py diff --git a/nova/api/openstack/contrib/multinic.py b/nova/api/openstack/contrib/multinic.py new file mode 100644 index 000000000..164af79b0 --- /dev/null +++ b/nova/api/openstack/contrib/multinic.py @@ -0,0 +1,83 @@ +# 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") + + +class Multinic(extensions.ExtensionDescriptor): + def __init__(self, *args, **kwargs): + super(Multinic, self).__init__(*args, **kwargs) + self.compute_api = compute.API() + + def get_name(self): + return "Multinic" + + def get_alias(self): + return "NMN" + + def get_description(self): + return "Multiple network support" + + def get_namespace(self): + return "http://docs.openstack.org/ext/multinic/api/v1.1" + + def get_updated(self): + return "2011-06-09T00:00:00+00:00" + + def get_actions(self): + 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): + # Not yet implemented + raise faults.Fault(exc.HTTPNotImplemented()) -- cgit From 87d90616df651787f52a59be37457335eb881ed7 Mon Sep 17 00:00:00 2001 From: Johannes Erdfelt Date: Wed, 15 Jun 2011 22:57:05 +0000 Subject: Workaround windows agent bugs where some responses have trailing \\r\\n --- nova/virt/xenapi/vmops.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/nova/virt/xenapi/vmops.py b/nova/virt/xenapi/vmops.py index 190bf7c20..5ca041059 100644 --- a/nova/virt/xenapi/vmops.py +++ b/nova/virt/xenapi/vmops.py @@ -526,7 +526,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) @@ -592,7 +594,9 @@ 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) # Send the encrypted password -- cgit From 46e016348ff4303310af328fa1af8fab513632c9 Mon Sep 17 00:00:00 2001 From: Johannes Erdfelt Date: Wed, 15 Jun 2011 23:11:00 +0000 Subject: Add trailing LF (\n) to password for compatibility with old agents --- nova/virt/xenapi/vmops.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/nova/virt/xenapi/vmops.py b/nova/virt/xenapi/vmops.py index 5ca041059..24e293a7b 100644 --- a/nova/virt/xenapi/vmops.py +++ b/nova/virt/xenapi/vmops.py @@ -526,8 +526,8 @@ class VMOps(object): # No response from the agent return resp_dict = json.loads(resp) - # Some old versions of the windows agent have a trailing \\r\\n - # (ie CRLF escaped) for some reason. Strip that off + # 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: @@ -594,11 +594,13 @@ class VMOps(object): # There was some sort of error; the message will contain # a description of the error. raise RuntimeError(resp_dict['message']) - # Some old versions of the windows agent have a trailing \\r\\n - # (ie CRLF escaped) for some reason. Strip that off + # 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} -- cgit From 220ddb2bd5413ea5fa2bff450f4fb3aba136e909 Mon Sep 17 00:00:00 2001 From: Isaku Yamahata Date: Thu, 16 Jun 2011 20:29:10 +0900 Subject: api/ec2: check user permission for start/stop instances This patch adds precise permission check for start/stop instances. --- nova/api/ec2/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/nova/api/ec2/__init__.py b/nova/api/ec2/__init__.py index 890d57fe7..857fa96c3 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'], -- cgit From 009874210e818752d5f206df01313242c728e5f8 Mon Sep 17 00:00:00 2001 From: Isaku Yamahata Date: Thu, 16 Jun 2011 20:29:10 +0900 Subject: api/ec2: check user permission for start/stop instances This patch adds precise permission check for start/stop instances. --- nova/api/ec2/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/nova/api/ec2/__init__.py b/nova/api/ec2/__init__.py index 1915d007d..b8f0dd576 100644 --- a/nova/api/ec2/__init__.py +++ b/nova/api/ec2/__init__.py @@ -261,6 +261,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'], -- cgit From df63c8e14da8c93453d4d7485829e38e0db30711 Mon Sep 17 00:00:00 2001 From: Isaku Yamahata Date: Thu, 16 Jun 2011 20:35:49 +0900 Subject: ec2utils: consolidate 'vol-%08x' and 'snap-%08x' By introducing helper functions, consolidate scattered '{vol, snap}-%08x' --- nova/api/ec2/__init__.py | 4 ++-- nova/api/ec2/cloud.py | 14 ++++++-------- nova/api/ec2/ec2utils.py | 11 +++++++++++ nova/tests/test_cloud.py | 16 ++++++++-------- 4 files changed, 27 insertions(+), 18 deletions(-) diff --git a/nova/api/ec2/__init__.py b/nova/api/ec2/__init__.py index 857fa96c3..35f67d39b 100644 --- a/nova/api/ec2/__init__.py +++ b/nova/api/ec2/__init__.py @@ -327,13 +327,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 a59173103..d36eb7a04 100644 --- a/nova/api/ec2/cloud.py +++ b/nova/api/ec2/cloud.py @@ -305,9 +305,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'] @@ -641,7 +640,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'] @@ -663,8 +662,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 @@ -727,7 +725,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) @@ -739,7 +737,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 == []: diff --git a/nova/api/ec2/ec2utils.py b/nova/api/ec2/ec2utils.py index 222e1de1e..bcdf2ba78 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]|$)))') diff --git a/nova/tests/test_cloud.py b/nova/tests/test_cloud.py index 6a6256c20..2efcd304d 100644 --- a/nova/tests/test_cloud.py +++ b/nova/tests/test_cloud.py @@ -171,7 +171,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) @@ -187,7 +187,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) @@ -224,7 +224,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) @@ -238,7 +238,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) @@ -255,7 +255,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) @@ -553,7 +553,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']) @@ -562,7 +562,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']) @@ -804,7 +804,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) -- cgit From 72730a3ba122a86f736513b9dab886bc4087bdc5 Mon Sep 17 00:00:00 2001 From: Isaku Yamahata Date: Thu, 16 Jun 2011 21:25:02 +0900 Subject: db/model: add root_device_name column to instances table The root_device_name column is necessary to support ec2 RootDeviceName. --- .../versions/022_add_root_device_name.py | 47 ++++++++++++++++++++++ nova/db/sqlalchemy/models.py | 2 + 2 files changed, 49 insertions(+) create mode 100644 nova/db/sqlalchemy/migrate_repo/versions/022_add_root_device_name.py diff --git a/nova/db/sqlalchemy/migrate_repo/versions/022_add_root_device_name.py b/nova/db/sqlalchemy/migrate_repo/versions/022_add_root_device_name.py new file mode 100644 index 000000000..6b98b9890 --- /dev/null +++ b/nova/db/sqlalchemy/migrate_repo/versions/022_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 23e1bd112..62293daba 100644 --- a/nova/db/sqlalchemy/models.py +++ b/nova/db/sqlalchemy/models.py @@ -234,6 +234,8 @@ class Instance(BASE, NovaBase): os_type = Column(String(255)) vm_mode = Column(String(255)) + 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 -- cgit From 1a54498af5bc2b81bf8bf6e3b9a4ad4cc2db79e2 Mon Sep 17 00:00:00 2001 From: Isaku Yamahata Date: Thu, 16 Jun 2011 21:25:35 +0900 Subject: api/ec2/image: support block device mapping This patch adds --block-device-mapping support for register image and instance creation. --- nova/api/ec2/cloud.py | 189 ++++++++++++++++++++++++++++++++++++++++------- nova/api/ec2/ec2utils.py | 19 +++++ nova/compute/api.py | 82 +++++++++++++++----- nova/compute/manager.py | 20 ++--- nova/image/s3.py | 25 +++++++ 5 files changed, 282 insertions(+), 53 deletions(-) diff --git a/nova/api/ec2/cloud.py b/nova/api/ec2/cloud.py index d36eb7a04..1e594cd73 100644 --- a/nova/api/ec2/cloud.py +++ b/nova/api/ec2/cloud.py @@ -27,6 +27,7 @@ import IPy import os import urllib import tempfile +import time import shutil from nova import compute @@ -75,6 +76,73 @@ 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..DeviceName + BlockDevicedMapping..Ebs.SnapshotId + BlockDevicedMapping..Ebs.VolumeSize + BlockDevicedMapping..Ebs.DeleteOnTermination + BlockDevicedMapping..Ebs.NoDevice + BlockDevicedMapping..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) + return bdm + + +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(mappings, result): + """Format multiple BlockDeviceMappingItemType""" + block_device_mapping = [_format_block_device_mapping(bdm) for bdm in + mappings] + if block_device_mapping: + result['blockDeviceMapping'] = block_device_mapping + + class CloudController(object): """ CloudController provides the critical dispatch between inbound API calls through the endpoint and messages @@ -177,7 +245,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', @@ -761,6 +829,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 @@ -822,6 +921,10 @@ class CloudController(object): i['amiLaunchIndex'] = instance['launch_index'] i['displayName'] = instance['display_name'] i['displayDescription'] = instance['display_description'] + i['rootDeviceName'] = (instance['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} @@ -907,24 +1010,7 @@ class CloudController(object): if kwargs.get('ramdisk_id'): ramdisk = self._get_image(context, kwargs['ramdisk_id']) kwargs['ramdisk_id'] = ramdisk['id'] - for bdm in kwargs.get('block_device_mapping', []): - # NOTE(yamahata) - # BlockDevicedMapping..DeviceName - # BlockDevicedMapping..Ebs.SnapshotId - # BlockDevicedMapping..Ebs.VolumeSize - # BlockDevicedMapping..Ebs.DeleteOnTermination - # BlockDevicedMapping..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']) @@ -1078,6 +1164,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.get('block_device_mapping', []), i) + return i def describe_images(self, context, image_id=None, **kwargs): @@ -1102,30 +1202,65 @@ 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'].get('block_device_mapping', []), 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, diff --git a/nova/api/ec2/ec2utils.py b/nova/api/ec2/ec2utils.py index bcdf2ba78..9839f8604 100644 --- a/nova/api/ec2/ec2utils.py +++ b/nova/api/ec2/ec2utils.py @@ -135,3 +135,22 @@ 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 + # .manifest.xml + if 'root_device_name' in properties: + root_device_name = properties['root_device_name'] + + return root_device_name diff --git a/nova/compute/api.py b/nova/compute/api.py index 18363ace0..d89dfb746 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 @@ -220,6 +221,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, @@ -243,10 +247,62 @@ class API(base.Base): 'metadata': metadata, 'availability_zone': availability_zone, 'os_type': os_type, - 'vm_mode': vm_mode} + 'vm_mode': vm_mode, + 'root_device_name': root_device_name} return (num_instances, base_options, security_groups) + 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 mappings: + LOG.debug(_("bdm %s"), bdm) + + virtual_name = bdm['virtualName'] + if virtual_name == 'ami' or virtual_name == 'root': + continue + + assert (virtual_name == 'swap' or + virtual_name.startswith('ephemeral')) + values = { + 'instance_id': instance_id, + 'device_name': bdm['deviceName'], + 'virtual_name': virtual_name, } + self.db.block_device_mapping_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 + + 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')} + + # 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_create(elevated_context, values) + def create_db_entry_for_new_instance(self, context, base_options, security_groups, block_device_mapping, num=1): """Create an entry in the DB for this new instance, @@ -268,22 +324,14 @@ class API(base.Base): instance_id, security_group_id) - # 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 = dict(hostname=self.hostname_factory(instance_id)) diff --git a/nova/compute/manager.py b/nova/compute/manager.py index 8ab744855..146cdbb9f 100644 --- a/nova/compute/manager.py +++ b/nova/compute/manager.py @@ -227,6 +227,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 @@ -259,15 +270,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 diff --git a/nova/image/s3.py b/nova/image/s3.py index 9e95bd698..b1afc6a81 100644 --- a/nova/image/s3.py +++ b/nova/image/s3.py @@ -141,6 +141,28 @@ class S3ImageService(service.BaseImageService): except Exception: arch = 'x86_64' + # NOTE(yamahata): + # EC2 ec2-budlne-image --block-device-mapping accepts + # = where + # virtual name = {ami, root, swap, ephemeral} + # 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 +173,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', -- cgit From 46c7e018ee3aed0f0e27ae3544193adb5fa62958 Mon Sep 17 00:00:00 2001 From: Isaku Yamahata Date: Thu, 16 Jun 2011 21:25:57 +0900 Subject: api/ec2: support CreateImage --- nova/api/ec2/__init__.py | 1 + nova/api/ec2/cloud.py | 101 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 102 insertions(+) diff --git a/nova/api/ec2/__init__.py b/nova/api/ec2/__init__.py index 35f67d39b..cf1734281 100644 --- a/nova/api/ec2/__init__.py +++ b/nova/api/ec2/__init__.py @@ -271,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) diff --git a/nova/api/ec2/cloud.py b/nova/api/ec2/cloud.py index 1e594cd73..b38061eaf 100644 --- a/nova/api/ec2/cloud.py +++ b/nova/api/ec2/cloud.py @@ -1291,3 +1291,104 @@ 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 + #description = kwargs.get('name') + #description = kwargs.get('description') + 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 + 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? + + src_image = self.image_service.show(context, instance['image_id']) + 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( + 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', []): + virtual_name = m['virtual'] + if virtual_name in ('ami', 'root'): + continue + + assert (virtual_name == 'swap' or + virtual_name.startswith('ephemeral')) + device_name = m['device_name'] + if device_name in [b.device_name for b in mapping + if not b.no_device]: + 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'): + del src_image[attr] + + image_id = self._register_image(context, src_image) + + if restart_instance: + self.compute_api.start(context, instance_id=instance_id) + + return {'imageId': image_id} -- cgit From 8da93c0352996ec733f3678b9ca02b72a96498c2 Mon Sep 17 00:00:00 2001 From: Isaku Yamahata Date: Thu, 16 Jun 2011 21:44:50 +0900 Subject: fix conflict with rebasing. --- .../versions/022_add_root_device_name.py | 47 ---------------------- .../versions/025_add_root_device_name.py | 47 ++++++++++++++++++++++ 2 files changed, 47 insertions(+), 47 deletions(-) delete mode 100644 nova/db/sqlalchemy/migrate_repo/versions/022_add_root_device_name.py create mode 100644 nova/db/sqlalchemy/migrate_repo/versions/025_add_root_device_name.py diff --git a/nova/db/sqlalchemy/migrate_repo/versions/022_add_root_device_name.py b/nova/db/sqlalchemy/migrate_repo/versions/022_add_root_device_name.py deleted file mode 100644 index 6b98b9890..000000000 --- a/nova/db/sqlalchemy/migrate_repo/versions/022_add_root_device_name.py +++ /dev/null @@ -1,47 +0,0 @@ -# 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/migrate_repo/versions/025_add_root_device_name.py b/nova/db/sqlalchemy/migrate_repo/versions/025_add_root_device_name.py new file mode 100644 index 000000000..6b98b9890 --- /dev/null +++ b/nova/db/sqlalchemy/migrate_repo/versions/025_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') -- cgit From 7bc76ef403507f6762f782ff4d305cf2718346d5 Mon Sep 17 00:00:00 2001 From: Isaku Yamahata Date: Thu, 16 Jun 2011 22:17:54 +0900 Subject: ec2/cloud.py: fix mismerge --- nova/api/ec2/cloud.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/nova/api/ec2/cloud.py b/nova/api/ec2/cloud.py index b38061eaf..1bf8bbd35 100644 --- a/nova/api/ec2/cloud.py +++ b/nova/api/ec2/cloud.py @@ -1010,7 +1010,8 @@ class CloudController(object): if kwargs.get('ramdisk_id'): ramdisk = self._get_image(context, kwargs['ramdisk_id']) kwargs['ramdisk_id'] = ramdisk['id'] - _parse_block_device_mapping(bdm) + for bdm in kwargs.get('block_device_mapping', []): + _parse_block_device_mapping(bdm) image = self._get_image(context, kwargs['image_id']) -- cgit From 774e201a04addf95fab2253998967b212588cb0a Mon Sep 17 00:00:00 2001 From: Isaku Yamahata Date: Thu, 16 Jun 2011 22:18:06 +0900 Subject: compute/api: fix mismerge due to instance creation change --- nova/compute/api.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/nova/compute/api.py b/nova/compute/api.py index d89dfb746..5b56d7865 100644 --- a/nova/compute/api.py +++ b/nova/compute/api.py @@ -250,7 +250,7 @@ class API(base.Base): 'vm_mode': vm_mode, 'root_device_name': root_device_name} - return (num_instances, base_options, security_groups) + return (num_instances, base_options, security_groups, image) def _update_image_block_device_mapping(self, elevated_context, instance_id, mappings): @@ -303,7 +303,7 @@ class API(base.Base): self.db.block_device_mapping_create(elevated_context, values) - def create_db_entry_for_new_instance(self, context, base_options, + def create_db_entry_for_new_instance(self, context, image, base_options, security_groups, block_device_mapping, num=1): """Create an entry in the DB for this new instance, including any related table updates (such as security @@ -393,7 +393,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, security_groups = \ + num_instances, base_options, security_groups, image = \ self._check_create_parameters( context, instance_type, image_href, kernel_id, ramdisk_id, @@ -429,7 +429,7 @@ class API(base.Base): Returns a list of instance dicts. """ - num_instances, base_options, security_groups = \ + num_instances, base_options, security_groups, image = \ self._check_create_parameters( context, instance_type, image_href, kernel_id, ramdisk_id, @@ -444,7 +444,7 @@ class API(base.Base): instances = [] LOG.debug(_("Going to run %s instances..."), num_instances) for num in range(num_instances): - instance = self.create_db_entry_for_new_instance(context, + instance = self.create_db_entry_for_new_instance(context, image, base_options, security_groups, block_device_mapping, num=num) instances.append(instance) -- cgit From 96fc985878cd52813aa07a4843e5928031b1501a Mon Sep 17 00:00:00 2001 From: Isaku Yamahata Date: Wed, 22 Jun 2011 12:40:06 +0900 Subject: db/migration: resolve version conflict --- .../versions/025_add_root_device_name.py | 47 ---------------------- .../versions/027_add_root_device_name.py | 47 ++++++++++++++++++++++ 2 files changed, 47 insertions(+), 47 deletions(-) delete mode 100644 nova/db/sqlalchemy/migrate_repo/versions/025_add_root_device_name.py create mode 100644 nova/db/sqlalchemy/migrate_repo/versions/027_add_root_device_name.py diff --git a/nova/db/sqlalchemy/migrate_repo/versions/025_add_root_device_name.py b/nova/db/sqlalchemy/migrate_repo/versions/025_add_root_device_name.py deleted file mode 100644 index 6b98b9890..000000000 --- a/nova/db/sqlalchemy/migrate_repo/versions/025_add_root_device_name.py +++ /dev/null @@ -1,47 +0,0 @@ -# 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/migrate_repo/versions/027_add_root_device_name.py b/nova/db/sqlalchemy/migrate_repo/versions/027_add_root_device_name.py new file mode 100644 index 000000000..6b98b9890 --- /dev/null +++ b/nova/db/sqlalchemy/migrate_repo/versions/027_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') -- cgit From 4020e0dab41caf22de629c94cf94f5ea2101faee Mon Sep 17 00:00:00 2001 From: Isaku Yamahata Date: Wed, 22 Jun 2011 12:48:30 +0900 Subject: db/block_device_mapping/api: introduce update_or_create introduce db.block_device_mapping_udpate_or_create() which update the colume if exists. Create new column if not existed. This api will be used later for block device mapping tracking. --- nova/db/api.py | 8 +++++++- nova/db/sqlalchemy/api.py | 17 +++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/nova/db/api.py b/nova/db/api.py index 8f8e856b8..d77d0c352 100644 --- a/nova/db/api.py +++ b/nova/db/api.py @@ -936,10 +936,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 a2500a38d..7108c7a7a 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -1932,6 +1932,23 @@ def block_device_mapping_update(context, bdm_id, values): update(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() -- cgit From 3a83471ec002127a84d319e397ce54e49bd696a1 Mon Sep 17 00:00:00 2001 From: Isaku Yamahata Date: Wed, 22 Jun 2011 12:53:47 +0900 Subject: api/ec2/image: make block device mapping pass unit tests. This patch makes pass unit tests which will follow later. --- nova/api/ec2/cloud.py | 30 +++++++++++++++++++++++------- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/nova/api/ec2/cloud.py b/nova/api/ec2/cloud.py index 8138567d0..703dd82b4 100644 --- a/nova/api/ec2/cloud.py +++ b/nova/api/ec2/cloud.py @@ -135,12 +135,29 @@ def _format_block_device_mapping(bdm): return item -def _format_mappings(mappings, result): +def _format_mappings(properties, result): """Format multiple BlockDeviceMappingItemType""" + mappings = [{'virtualName': m['virtual'], 'deviceName': m['device']} + for m in properties.get('mappings', []) + if (m['virtual'] == 'swap' or + m['virtual'].startswith('ephemeral'))] + block_device_mapping = [_format_block_device_mapping(bdm) for bdm in - mappings] - if block_device_mapping: - result['blockDeviceMapping'] = block_device_mapping + 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): @@ -1178,7 +1195,7 @@ class CloudController(object): i['rootDeviceName'] = (root_device_name or _DEFAULT_ROOT_DEVICE_NAME) i['rootDeviceType'] = root_device_type - _format_mappings(properties.get('block_device_mapping', []), i) + _format_mappings(properties, i) return i @@ -1232,8 +1249,7 @@ class CloudController(object): def describe_image_attribute(self, context, image_id, attribute, **kwargs): def _block_device_mapping_attribute(image, result): - _format_mappings( - image['properties'].get('block_device_mapping', []), result) + _format_mappings(image['properties'], result) def _launch_permission_attribute(image, result): result['launchPermission'] = [] -- cgit From 91cc2d5f974d67d91e1e783aaec105c489a47cce Mon Sep 17 00:00:00 2001 From: Isaku Yamahata Date: Wed, 22 Jun 2011 12:54:22 +0900 Subject: volume/api: introduce create_snapshot_force() Introduce create_snapshot_force() which create snapshot even when the volume is in in-use. This is needed for CreateImage with no_reboot=true. --- nova/compute/api.py | 24 +++++++++++------------- nova/volume/api.py | 13 +++++++++++-- 2 files changed, 22 insertions(+), 15 deletions(-) diff --git a/nova/compute/api.py b/nova/compute/api.py index e2692a42f..884ec9198 100644 --- a/nova/compute/api.py +++ b/nova/compute/api.py @@ -264,7 +264,7 @@ class API(base.Base): for bdm in mappings: LOG.debug(_("bdm %s"), bdm) - virtual_name = bdm['virtualName'] + virtual_name = bdm['virtual'] if virtual_name == 'ami' or virtual_name == 'root': continue @@ -272,9 +272,10 @@ class API(base.Base): virtual_name.startswith('ephemeral')) values = { 'instance_id': instance_id, - 'device_name': bdm['deviceName'], + 'device_name': bdm['device'], 'virtual_name': virtual_name, } - self.db.block_device_mapping_create(elevated_context, values) + self.db.block_device_mapping_update_or_create(elevated_context, + values) def _update_block_device_mapping(self, elevated_context, instance_id, block_device_mapping): @@ -285,15 +286,11 @@ class API(base.Base): 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')} + 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. @@ -305,7 +302,8 @@ class API(base.Base): 'virtual_name'): values[k] = None - self.db.block_device_mapping_create(elevated_context, values) + self.db.block_device_mapping_update_or_create(elevated_context, + values) def create_db_entry_for_new_instance(self, context, image, base_options, security_groups, block_device_mapping, num=1): 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": -- cgit From d81d75bec04fe19492544e5bf7548dce5a2366ad Mon Sep 17 00:00:00 2001 From: Isaku Yamahata Date: Wed, 22 Jun 2011 12:54:22 +0900 Subject: api/ec2: make CreateImage pass unit tests --- nova/api/ec2/cloud.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/nova/api/ec2/cloud.py b/nova/api/ec2/cloud.py index 703dd82b4..c25db9014 100644 --- a/nova/api/ec2/cloud.py +++ b/nova/api/ec2/cloud.py @@ -1346,7 +1346,7 @@ class CloudController(object): state_description = instance['state_description'] # NOTE(yamahata): timeout and error? - src_image = self.image_service.show(context, instance['image_id']) + 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'] @@ -1372,7 +1372,7 @@ class CloudController(object): # 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( + 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'] @@ -1388,9 +1388,9 @@ class CloudController(object): assert (virtual_name == 'swap' or virtual_name.startswith('ephemeral')) - device_name = m['device_name'] - if device_name in [b.device_name for b in mapping - if not b.no_device]: + 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 @@ -1402,7 +1402,7 @@ class CloudController(object): properties['block_device_mapping'] = mapping for attr in ('status', 'location', 'id'): - del src_image[attr] + src_image.pop(attr, None) image_id = self._register_image(context, src_image) -- cgit From 181ae36fe34edd206c33e3a0b7e10800ced93e97 Mon Sep 17 00:00:00 2001 From: Isaku Yamahata Date: Wed, 22 Jun 2011 12:54:32 +0900 Subject: test_api: unit tests for ec2utils.id_to_ec2_{snap, vol}_id() --- nova/tests/test_api.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/nova/tests/test_api.py b/nova/tests/test_api.py index 20b20fcbf..e74ae839e 100644 --- a/nova/tests/test_api.py +++ b/nova/tests/test_api.py @@ -107,6 +107,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,7 +118,8 @@ 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') class ApiEc2TestCase(test.TestCase): """Unit test for the cloud controller on an EC2 API""" -- cgit From 79d97f7232c119496dde1dd2f0534520ab383962 Mon Sep 17 00:00:00 2001 From: Isaku Yamahata Date: Wed, 22 Jun 2011 12:54:39 +0900 Subject: ec2utils: add an unit test for dict_from_dotted_str() --- nova/tests/test_api.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/nova/tests/test_api.py b/nova/tests/test_api.py index e74ae839e..20d78687e 100644 --- a/nova/tests/test_api.py +++ b/nova/tests/test_api.py @@ -121,6 +121,26 @@ class Ec2utilsTestCase(test.TestCase): 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) + + class ApiEc2TestCase(test.TestCase): """Unit test for the cloud controller on an EC2 API""" def setUp(self): -- cgit From 776571b38fb898a4dafa80e8f3da34b214c948b8 Mon Sep 17 00:00:00 2001 From: Isaku Yamahata Date: Wed, 22 Jun 2011 12:54:43 +0900 Subject: ec2utils: unit tests for case insensitive true/false conversion --- nova/tests/test_api.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/nova/tests/test_api.py b/nova/tests/test_api.py index 20d78687e..bea4b4e9d 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) -- cgit From ec515fa667e2954aa93a6954a541739e6e3aa221 Mon Sep 17 00:00:00 2001 From: Isaku Yamahata Date: Wed, 22 Jun 2011 12:54:49 +0900 Subject: image/s3: factor out _s3_create() for testability The unittest will come with later changeset. --- nova/image/s3.py | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/nova/image/s3.py b/nova/image/s3.py index b1afc6a81..86dd2e309 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' @@ -183,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(metadata, context, manifest) image_id = image['id'] def delayed_create(): -- cgit From ac9ed64077eaad6b4df91fbf90af7933a6bddd5a Mon Sep 17 00:00:00 2001 From: Isaku Yamahata Date: Wed, 22 Jun 2011 12:54:49 +0900 Subject: unittest, image/s3: unit tests for s3 image handler --- nova/tests/image/test_s3.py | 122 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 122 insertions(+) create mode 100644 nova/tests/image/test_s3.py 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 = """ + + 2011-06-17 + + test-s3 + 0 + 0 + + + x86_64 + + + ami + sda1 + + + root + /dev/sda1 + + + ephemeral0 + sda2 + + + swap + sda3 + + + + +""" + + +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) -- cgit From aadb9a0a8a9a0b947643c04f24b623412db7d48d Mon Sep 17 00:00:00 2001 From: Isaku Yamahata Date: Wed, 22 Jun 2011 12:54:56 +0900 Subject: ec2utils: an unit test for ec2utils.properties_root_defice_name. --- nova/tests/test_api.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/nova/tests/test_api.py b/nova/tests/test_api.py index bea4b4e9d..ebc5508cc 100644 --- a/nova/tests/test_api.py +++ b/nova/tests/test_api.py @@ -142,6 +142,17 @@ class Ec2utilsTestCase(test.TestCase): 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') + class ApiEc2TestCase(test.TestCase): """Unit test for the cloud controller on an EC2 API""" -- cgit From 1df8275883930c71ea4324b0d43b6508440e1d65 Mon Sep 17 00:00:00 2001 From: Isaku Yamahata Date: Wed, 22 Jun 2011 12:55:03 +0900 Subject: test_cloud: an unit test for describe image with block device mapping --- nova/tests/test_cloud.py | 133 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 131 insertions(+), 2 deletions(-) diff --git a/nova/tests/test_cloud.py b/nova/tests/test_cloud.py index fd1c21386..35660d4bb 100644 --- a/nova/tests/test_cloud.py +++ b/nova/tests/test_cloud.py @@ -323,6 +323,135 @@ 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 _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 + + # 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 + mappings1 = [ + {'device': '/dev/sda1', 'virtual': 'root'}, + + {'device': '/dev/sdb0', 'virtual': 'ephemeral0'}, + {'device': '/dev/sdb1', 'virtual': 'ephemeral1'}, + {'device': '/dev/sdb2', 'virtual': 'ephemeral2'}, + {'device': '/dev/sdb3', 'virtual': 'ephemeral3'}, + {'device': '/dev/sdb4', 'virtual': 'ephemeral4'}, + + {'device': '/dev/sdc0', 'virtual': 'swap'}, + {'device': '/dev/sdc1', 'virtual': 'swap'}, + {'device': '/dev/sdc2', 'virtual': 'swap'}, + {'device': '/dev/sdc3', 'virtual': 'swap'}, + {'device': '/dev/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', + '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) + + result = describe_images(self.context, ['ami-00000001']) + result = self._assertImageSet(result, 'instance-store', '/dev/sda1') + + # NOTE(yamahata): noDevice doesn't make sense when returning mapping + # It makes sense only when user overriding existing + # mapping. + expected_bdms = [ + {'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} + ] + self.assertDictListUnorderedMatch(result['blockDeviceMapping'], + expected_bdms, 'deviceName') + + result = describe_images(self.context, ['ami-00000002']) + result = self._assertImageSet(result, 'ebs', '/dev/sdb1') + + expected_bdms = [{'deviceName': '/dev/sdb1', + 'ebs': {'snapshotId': 'snap-00053977'}}] + self.assertDictListUnorderedMatch(result['blockDeviceMapping'], + expected_bdms, 'deviceName') + + self.stubs.UnsetAll() + def test_describe_image_attribute(self): describe_image_attribute = self.cloud.describe_image_attribute @@ -679,10 +808,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) -- cgit From c6792450aa745ef003b80999eae3283533a15521 Mon Sep 17 00:00:00 2001 From: Isaku Yamahata Date: Wed, 22 Jun 2011 12:55:08 +0900 Subject: unittest: an unit test for ec2 describe image attribute --- nova/tests/test_cloud.py | 141 +++++++++++++++++++++++++++++------------------ 1 file changed, 88 insertions(+), 53 deletions(-) diff --git a/nova/tests/test_cloud.py b/nova/tests/test_cloud.py index 35660d4bb..43bcdf703 100644 --- a/nova/tests/test_cloud.py +++ b/nova/tests/test_cloud.py @@ -332,32 +332,7 @@ class CloudTestCase(test.TestCase): if d1[key] == d2[key]: self.assertDictMatch(d1, d2) - 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 - - # 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 + def _setUpImageSet(self): mappings1 = [ {'device': '/dev/sda1', 'virtual': 'root'}, @@ -416,39 +391,73 @@ class CloudTestCase(test.TestCase): self.stubs.Set(fake._FakeImageService, 'show', fake_show) self.stubs.Set(fake._FakeImageService, 'detail', fake_detail) + 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', '/dev/sda1') - - # NOTE(yamahata): noDevice doesn't make sense when returning mapping - # It makes sense only when user overriding existing - # mapping. - expected_bdms = [ - {'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} - ] + result = self._assertImageSet(result, 'instance-store', + self._expected_root_device_name1) + self.assertDictListUnorderedMatch(result['blockDeviceMapping'], - expected_bdms, 'deviceName') + self._expected_bdms1, 'deviceName') result = describe_images(self.context, ['ami-00000002']) - result = self._assertImageSet(result, 'ebs', '/dev/sdb1') + result = self._assertImageSet(result, 'ebs', + self._expected_root_device_name2) - expected_bdms = [{'deviceName': '/dev/sdb1', - 'ebs': {'snapshotId': 'snap-00053977'}}] self.assertDictListUnorderedMatch(result['blockDeviceMapping'], - expected_bdms, 'deviceName') + self._expected_bdms2, 'deviceName') self.stubs.UnsetAll() @@ -465,6 +474,32 @@ 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 -- cgit From 5276e80c403a2ae87d3c93979289331e286fd2a1 Mon Sep 17 00:00:00 2001 From: Isaku Yamahata Date: Wed, 22 Jun 2011 12:55:11 +0900 Subject: api/ec2, boot-from-volume: an unit test for describe instances --- nova/test.py | 12 ++++ nova/tests/test_cloud.py | 140 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 152 insertions(+) diff --git a/nova/test.py b/nova/test.py index 4a0a18fe7..99d4cec4f 100644 --- a/nova/test.py +++ b/nova/test.py @@ -252,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/test_cloud.py b/nova/tests/test_cloud.py index 43bcdf703..f7326e66f 100644 --- a/nova/tests/test_cloud.py +++ b/nova/tests/test_cloud.py @@ -294,6 +294,146 @@ class CloudTestCase(test.TestCase): db.service_destroy(self.context, comp1['id']) db.service_destroy(self.context, comp2['id']) + 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 (ec2_instance_id, 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 = 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'] + mappings = [ + {'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'}, + {'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 = [] + for bdm in mappings: + db.block_device_mapping_create(self.context, bdm) + if bdm.get('volume_id'): + values = {'volume_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.get(bdm_key): + 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) + + ec2_instance_id, result = self._assertInstance(instance_id) + expected_result = {'instanceId': ec2_instance_id, + 'rootDeviceName': '/dev/sdb1', + 'rootDeviceType': 'ebs'} + expected_block_device_mapping = [ + {'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. + self.assertSubDictMatch(expected_result, result) + self._assertEqualBlockDeviceMapping(expected_block_device_mapping, + result['blockDeviceMapping']) + + ec2_instance_id, result = self._assertInstance(inst2['id']) + expected_result = {'instanceId': ec2_instance_id, + 'rootDeviceName': '/dev/sdc1', + 'rootDeviceType': 'instance-store'} + self.assertSubDictMatch(expected_result, result) + + for vol in volumes: + db.volume_destroy(self.context, vol['id']) + for bdm in db.block_device_mapping_get_all_by_instance(self.context, + instance_id): + db.block_device_mapping_destroy(self.context, bdm['id']) + db.instance_destroy(self.context, inst2['id']) + db.instance_destroy(self.context, inst1['id']) + def test_describe_images(self): describe_images = self.cloud.describe_images -- cgit From 97a710fa191b0abd94fef25d7110448c41c4e259 Mon Sep 17 00:00:00 2001 From: Isaku Yamahata Date: Wed, 22 Jun 2011 12:55:14 +0900 Subject: api/ec2: an unit test for create image unit test for ec2 create image. This is incomplete as there is no unit test for register image. --- nova/tests/test_cloud.py | 90 ++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 72 insertions(+), 18 deletions(-) diff --git a/nova/tests/test_cloud.py b/nova/tests/test_cloud.py index f7326e66f..aed5aff6a 100644 --- a/nova/tests/test_cloud.py +++ b/nova/tests/test_cloud.py @@ -294,6 +294,24 @@ 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 _assertInstance(self, instance_id): ec2_instance_id = ec2utils.id_to_ec2_id(instance_id) result = self.cloud.describe_instances(self.context, @@ -334,7 +352,8 @@ class CloudTestCase(test.TestCase): 'volume_id': '2'}, {'instance_id': instance_id, 'device_name': '/dev/sdb2', - 'volume_id': '3'}, + 'volume_id': '3', + 'volume_size': 1}, {'instance_id': instance_id, 'device_name': '/dev/sdb3', 'delete_on_termination': True, @@ -365,21 +384,7 @@ class CloudTestCase(test.TestCase): 'device_name': '/dev/sdb9', 'virtual_name': 'ephemeral3'}] - volumes = [] - for bdm in mappings: - db.block_device_mapping_create(self.context, bdm) - if bdm.get('volume_id'): - values = {'volume_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.get(bdm_key): - 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) + volumes = self._block_device_mapping_create(instance_id, mappings) ec2_instance_id, result = self._assertInstance(instance_id) expected_result = {'instanceId': ec2_instance_id, @@ -472,7 +477,7 @@ class CloudTestCase(test.TestCase): if d1[key] == d2[key]: self.assertDictMatch(d1, d2) - def _setUpImageSet(self): + def _setUpImageSet(self, create_volumes_and_snapshots=False): mappings1 = [ {'device': '/dev/sda1', 'virtual': 'root'}, @@ -502,6 +507,7 @@ class CloudTestCase(test.TestCase): 'properties': { 'kernel_id': 1, 'type': 'machine', + 'image_state': 'available', 'mappings': mappings1, 'block_device_mapping': block_device_mapping1, } @@ -531,6 +537,22 @@ class CloudTestCase(test.TestCase): 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] @@ -951,11 +973,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): @@ -1175,3 +1199,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() -- cgit From c24b30c9a060e50c7bd953a7d68c409416f4f752 Mon Sep 17 00:00:00 2001 From: Isaku Yamahata Date: Thu, 23 Jun 2011 19:51:00 +0900 Subject: volume/api: an unit test for create_snapshot_force() --- nova/tests/test_volume.py | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/nova/tests/test_volume.py b/nova/tests/test_volume.py index 4f10ee6af..5230cef0e 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): @@ -224,6 +231,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.""" -- cgit From 1c4a3e14a0cef6938c477908d5c3bfe5ddf0e07b Mon Sep 17 00:00:00 2001 From: Isaku Yamahata Date: Thu, 23 Jun 2011 19:51:00 +0900 Subject: ec2utils: introduce helper function to prepend '/dev/' in mappings Introduce a helper function to prepend /dev/ to device name in block device mapping of bundle --- nova/api/ec2/ec2utils.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/nova/api/ec2/ec2utils.py b/nova/api/ec2/ec2utils.py index 9839f8604..bae1e0ee5 100644 --- a/nova/api/ec2/ec2utils.py +++ b/nova/api/ec2/ec2utils.py @@ -154,3 +154,13 @@ def properties_root_device_name(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 -- cgit From 4b5fdb2ee109960be6b3ff1fa8068ab3ec428283 Mon Sep 17 00:00:00 2001 From: Isaku Yamahata Date: Thu, 23 Jun 2011 19:51:00 +0900 Subject: ec2: bundle block device mapping device name in block device mapping of bundle doesn't necessary carry "/dev/". So prepend it before processing. --- nova/api/ec2/cloud.py | 25 +++++++++++++++---------- nova/compute/api.py | 2 +- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/nova/api/ec2/cloud.py b/nova/api/ec2/cloud.py index c25db9014..8bf0950b2 100644 --- a/nova/api/ec2/cloud.py +++ b/nova/api/ec2/cloud.py @@ -92,20 +92,25 @@ def _parse_block_device_mapping(bdm): """ 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) + 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': , ...} + {'device_name': '...', 'snapshot_id': , ...} => BlockDeviceMappingItemType """ keys = (('deviceName', 'device_name'), @@ -138,7 +143,7 @@ def _format_block_device_mapping(bdm): def _format_mappings(properties, result): """Format multiple BlockDeviceMappingItemType""" mappings = [{'virtualName': m['virtual'], 'deviceName': m['device']} - for m in properties.get('mappings', []) + for m in _properties_get_mappings(properties) if (m['virtual'] == 'swap' or m['virtual'].startswith('ephemeral'))] @@ -1381,7 +1386,7 @@ class CloudController(object): if m: mapping.append(m) - for m in properties.get('mappings', []): + for m in _properties_get_mappings(properties): virtual_name = m['virtual'] if virtual_name in ('ami', 'root'): continue diff --git a/nova/compute/api.py b/nova/compute/api.py index 884ec9198..b3635d71f 100644 --- a/nova/compute/api.py +++ b/nova/compute/api.py @@ -261,7 +261,7 @@ class API(base.Base): """tell vm driver to create ephemeral/swap device at boot time by updating BlockDeviceMapping """ - for bdm in mappings: + for bdm in ec2utils.mappings_prepend_dev(mappings): LOG.debug(_("bdm %s"), bdm) virtual_name = bdm['virtual'] -- cgit From 8e3da07f2af1fb4c0d5fcb58cb6747afaa6b76d8 Mon Sep 17 00:00:00 2001 From: Isaku Yamahata Date: Thu, 23 Jun 2011 19:51:00 +0900 Subject: ec2utils: an unit test for mapping_prepend_dev() --- nova/tests/test_api.py | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/nova/tests/test_api.py b/nova/tests/test_api.py index ebc5508cc..26ac5ff24 100644 --- a/nova/tests/test_api.py +++ b/nova/tests/test_api.py @@ -153,6 +153,40 @@ class Ec2utilsTestCase(test.TestCase): 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): """Unit test for the cloud controller on an EC2 API""" -- cgit From c5761c6e983e539e5bb24ae6c0f3ea88faea676f Mon Sep 17 00:00:00 2001 From: Isaku Yamahata Date: Thu, 23 Jun 2011 19:51:01 +0900 Subject: ec2/cloud: an unit test for _format_instance_bdm() --- nova/tests/test_cloud.py | 206 +++++++++++++++++++++++++++-------------------- 1 file changed, 120 insertions(+), 86 deletions(-) diff --git a/nova/tests/test_cloud.py b/nova/tests/test_cloud.py index aed5aff6a..a179899ca 100644 --- a/nova/tests/test_cloud.py +++ b/nova/tests/test_cloud.py @@ -312,31 +312,7 @@ class CloudTestCase(test.TestCase): volumes.append(vol) return 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 (ec2_instance_id, 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 - """ + def _setUpBlockDeviceMapping(self): inst1 = db.instance_create(self.context, {'image_ref': 1, 'root_device_name': '/dev/sdb1'}) @@ -345,7 +321,7 @@ class CloudTestCase(test.TestCase): 'root_device_name': '/dev/sdc1'}) instance_id = inst1['id'] - mappings = [ + mappings0 = [ {'instance_id': instance_id, 'device_name': '/dev/sdb1', 'snapshot_id': '1', @@ -384,61 +360,119 @@ class CloudTestCase(test.TestCase): 'device_name': '/dev/sdb9', 'virtual_name': 'ephemeral3'}] - volumes = self._block_device_mapping_create(instance_id, mappings) - - ec2_instance_id, result = self._assertInstance(instance_id) - expected_result = {'instanceId': ec2_instance_id, - 'rootDeviceName': '/dev/sdb1', - 'rootDeviceType': 'ebs'} - expected_block_device_mapping = [ - {'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. - self.assertSubDictMatch(expected_result, result) - self._assertEqualBlockDeviceMapping(expected_block_device_mapping, - result['blockDeviceMapping']) - - ec2_instance_id, result = self._assertInstance(inst2['id']) - expected_result = {'instanceId': ec2_instance_id, - 'rootDeviceName': '/dev/sdc1', - 'rootDeviceType': 'instance-store'} - self.assertSubDictMatch(expected_result, result) + 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 bdm in db.block_device_mapping_get_all_by_instance(self.context, - instance_id): - db.block_device_mapping_destroy(self.context, bdm['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 @@ -481,17 +515,17 @@ class CloudTestCase(test.TestCase): mappings1 = [ {'device': '/dev/sda1', 'virtual': 'root'}, - {'device': '/dev/sdb0', 'virtual': 'ephemeral0'}, - {'device': '/dev/sdb1', 'virtual': 'ephemeral1'}, - {'device': '/dev/sdb2', 'virtual': 'ephemeral2'}, - {'device': '/dev/sdb3', 'virtual': 'ephemeral3'}, - {'device': '/dev/sdb4', 'virtual': 'ephemeral4'}, - - {'device': '/dev/sdc0', 'virtual': 'swap'}, - {'device': '/dev/sdc1', 'virtual': 'swap'}, - {'device': '/dev/sdc2', 'virtual': 'swap'}, - {'device': '/dev/sdc3', 'virtual': 'swap'}, - {'device': '/dev/sdc4', 'virtual': 'swap'}] + {'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}, -- cgit From c13bb7c3bf2400c45d1b93141e67916c81296e38 Mon Sep 17 00:00:00 2001 From: Isaku Yamahata Date: Thu, 23 Jun 2011 19:51:01 +0900 Subject: ec2/cloud: unit tests for parser/formatter of block device mapping This patch adds several unit tests for private functions in ec2/cloud.py. Which are used to parse/format block device mapping. _parse_block_device_mapping(), _format_block_device_mapping() and _format_mappings() --- nova/tests/test_bdm.py | 233 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 233 insertions(+) create mode 100644 nova/tests/test_bdm.py 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()) -- cgit From fd577b786c3a929300ae744858b57ccfed4fb2fc Mon Sep 17 00:00:00 2001 From: Isaku Yamahata Date: Thu, 23 Jun 2011 19:51:01 +0900 Subject: compute/api: an unit test for _update_{image_}bdm an unit test for _update_image_block_device_mapping() and _update_block_device_mapping() --- nova/tests/test_compute.py | 111 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 111 insertions(+) diff --git a/nova/tests/test_compute.py b/nova/tests/test_compute.py index 439508b27..783261127 100644 --- a/nova/tests/test_compute.py +++ b/nova/tests/test_compute.py @@ -721,3 +721,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) -- cgit From 8a884121e6a7c5f03f51266632bb671603c9c9a0 Mon Sep 17 00:00:00 2001 From: Isaku Yamahata Date: Thu, 23 Jun 2011 19:51:01 +0900 Subject: ec2/cloud: address review. - eliminated commented out lines in create_image() - added time out to create_image() , --- nova/api/ec2/cloud.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/nova/api/ec2/cloud.py b/nova/api/ec2/cloud.py index 8bf0950b2..b6274df0d 100644 --- a/nova/api/ec2/cloud.py +++ b/nova/api/ec2/cloud.py @@ -1323,8 +1323,6 @@ class CloudController(object): def create_image(self, context, instance_id, **kwargs): # NOTE(yamahata): name/description are ignored by register_image(), # do so here - #description = kwargs.get('name') - #description = kwargs.get('description') no_reboot = kwargs.get('no_reboot', False) ec2_instance_id = instance_id @@ -1345,11 +1343,18 @@ class CloudController(object): 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? + # 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'] -- cgit From c0a750757b2b6121bb04c6355a01cb31967c15e2 Mon Sep 17 00:00:00 2001 From: Isaku Yamahata Date: Fri, 24 Jun 2011 19:08:26 +0900 Subject: image/s3: typo --- nova/image/s3.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nova/image/s3.py b/nova/image/s3.py index 86dd2e309..4a3df98ba 100644 --- a/nova/image/s3.py +++ b/nova/image/s3.py @@ -186,7 +186,7 @@ class S3ImageService(service.BaseImageService): key = bucket.get_key(manifest_path) manifest = key.get_contents_as_string() - manifest, image = self._s3_parse_manifest(metadata, context, manifest) + manifest, image = self._s3_parse_manifest(context, metadata, manifest) image_id = image['id'] def delayed_create(): -- cgit From ce3b1ffec9bab02e2988b69e7e361d76e56ec002 Mon Sep 17 00:00:00 2001 From: Isaku Yamahata Date: Fri, 24 Jun 2011 19:08:26 +0900 Subject: ec2/cloud: typo --- nova/api/ec2/cloud.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nova/api/ec2/cloud.py b/nova/api/ec2/cloud.py index b6274df0d..a0a7db1f2 100644 --- a/nova/api/ec2/cloud.py +++ b/nova/api/ec2/cloud.py @@ -943,7 +943,7 @@ class CloudController(object): i['amiLaunchIndex'] = instance['launch_index'] i['displayName'] = instance['display_name'] i['displayDescription'] = instance['display_description'] - i['rootDeviceName'] = (instance['root_device_name'] or + i['rootDeviceName'] = (instance.get('root_device_name') or _DEFAULT_ROOT_DEVICE_NAME) self._format_instance_bdm(context, instance_id, i['rootDeviceName'], i) -- cgit From 14a63fa2c7de79fe173771fd98e448650387e924 Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Sat, 25 Jun 2011 17:29:14 -0700 Subject: add support to list security groups --- nova/api/ec2/cloud.py | 5 ++++- nova/api/ec2/metadatarequesthandler.py | 6 ++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/nova/api/ec2/cloud.py b/nova/api/ec2/cloud.py index 9aaf37a2d..194ddee97 100644 --- a/nova/api/ec2/cloud.py +++ b/nova/api/ec2/cloud.py @@ -167,6 +167,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': { @@ -190,7 +193,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']: 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) -- cgit From 1c677ad263f72583748074d01f6dbd384c411c11 Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Sat, 25 Jun 2011 17:47:54 -0700 Subject: add fake connection object to wsgi app --- nova/wsgi.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/nova/wsgi.py b/nova/wsgi.py index 33ba852bc..2613357b2 100644 --- a/nova/wsgi.py +++ b/nova/wsgi.py @@ -91,6 +91,35 @@ class Request(webob.Request): class Application(object): """Base WSGI application wrapper. Subclasses need to implement __call__.""" + @classmethod + def fake_connection(cls): + """Return a fake httplib connection object for this Application""" + class FakeConnection(object): + """Faked version of httplib.HTTPConnection to talk to app.""" + def __init__(self): + self.app = cls() + + def request(self, method, url, body=None, headers={}): + self.req = webob.Request.blank("/" + url.lstrip("/")) + self.req.remote_addr = '127.0.0.1' + self.req.method = method + if headers: + self.req.headers = headers + if body: + self.req.body = body + + def getresponse(self): + res = self.req.get_response(self.app) + + # httplib.Response has a read() method...fake it out + def fake_reader(): + return res.body + + setattr(res, 'read', fake_reader) + return res + + return FakeConnection() + @classmethod def factory(cls, global_config, **local_config): """Used for paste app factories in paste.deploy config files. -- cgit From a8f485d148f2184253fcbd7ccdfa9de9bb0bb735 Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Sat, 25 Jun 2011 17:50:39 -0700 Subject: add metadata tests --- nova/tests/test_metadata.py | 82 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 nova/tests/test_metadata.py diff --git a/nova/tests/test_metadata.py b/nova/tests/test_metadata.py new file mode 100644 index 000000000..30b5b19a7 --- /dev/null +++ b/nova/tests/test_metadata.py @@ -0,0 +1,82 @@ +# 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 + +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.conn = self.fake_connection() + 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) + + def real_connection(self): + router = metadatarequesthandler.MetadataRequestHandler() + service = wsgi.Server() + service.start(router, 16969) + return httplib.HTTPConnection('127.0.0.1', 16969) + + def fake_connection(self): + return metadatarequesthandler.MetadataRequestHandler.fake_connection() + + def request(self, relative_url): + self.conn.request('GET', relative_url) + return self.conn.getresponse().read() + + 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') + -- cgit From b86b14ac5a96332beedf10ca8da5787adfc6c308 Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Sat, 25 Jun 2011 17:52:26 -0700 Subject: pep8 --- nova/tests/test_metadata.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/nova/tests/test_metadata.py b/nova/tests/test_metadata.py index 30b5b19a7..1ef1fea86 100644 --- a/nova/tests/test_metadata.py +++ b/nova/tests/test_metadata.py @@ -40,7 +40,7 @@ class MetadataTestCase(test.TestCase): 'reservation_id': 'r-xxxxxxxx', 'user_data': '', 'image_ref': 7, - 'hostname' : 'test'}) + 'hostname': 'test'}) def instance_get(*args, **kwargs): return self.instance @@ -79,4 +79,3 @@ class MetadataTestCase(test.TestCase): self.stubs.Set(api, 'security_group_get_by_instance', sg_get) self.assertEqual(self.request('/meta-data/security-groups'), 'default\nother') - -- cgit From 02c0bf3b242395e63baf582b1f9c279eef4282d6 Mon Sep 17 00:00:00 2001 From: Isaku Yamahata Date: Mon, 27 Jun 2011 20:21:40 +0900 Subject: sqlalchmey/migration: resolved version conflict --- .../versions/027_add_root_device_name.py | 47 ---------------------- .../versions/028_add_root_device_name.py | 47 ++++++++++++++++++++++ 2 files changed, 47 insertions(+), 47 deletions(-) delete mode 100644 nova/db/sqlalchemy/migrate_repo/versions/027_add_root_device_name.py create mode 100644 nova/db/sqlalchemy/migrate_repo/versions/028_add_root_device_name.py diff --git a/nova/db/sqlalchemy/migrate_repo/versions/027_add_root_device_name.py b/nova/db/sqlalchemy/migrate_repo/versions/027_add_root_device_name.py deleted file mode 100644 index 6b98b9890..000000000 --- a/nova/db/sqlalchemy/migrate_repo/versions/027_add_root_device_name.py +++ /dev/null @@ -1,47 +0,0 @@ -# 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/migrate_repo/versions/028_add_root_device_name.py b/nova/db/sqlalchemy/migrate_repo/versions/028_add_root_device_name.py new file mode 100644 index 000000000..6b98b9890 --- /dev/null +++ b/nova/db/sqlalchemy/migrate_repo/versions/028_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') -- cgit From d1b904d5b7d4a277adc156d8ab576b37b7e190fc Mon Sep 17 00:00:00 2001 From: Brian Lamar Date: Mon, 27 Jun 2011 09:56:31 -0400 Subject: Use webob to test WSGI app --- nova/tests/test_metadata.py | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/nova/tests/test_metadata.py b/nova/tests/test_metadata.py index 1ef1fea86..c7095914c 100644 --- a/nova/tests/test_metadata.py +++ b/nova/tests/test_metadata.py @@ -21,6 +21,8 @@ import base64 import httplib +import webob + from nova import test from nova import wsgi from nova.api.ec2 import metadatarequesthandler @@ -48,23 +50,15 @@ class MetadataTestCase(test.TestCase): def floating_get(*args, **kwargs): return '99.99.99.99' - self.conn = self.fake_connection() 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) - - def real_connection(self): - router = metadatarequesthandler.MetadataRequestHandler() - service = wsgi.Server() - service.start(router, 16969) - return httplib.HTTPConnection('127.0.0.1', 16969) - - def fake_connection(self): - return metadatarequesthandler.MetadataRequestHandler.fake_connection() + self.app = metadatarequesthandler.MetadataRequestHandler() def request(self, relative_url): - self.conn.request('GET', relative_url) - return self.conn.getresponse().read() + 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') -- cgit From ad19a9e762f735a33af710fb8bded3a086266587 Mon Sep 17 00:00:00 2001 From: Brian Lamar Date: Mon, 27 Jun 2011 09:58:17 -0400 Subject: Removed now un-needed fake_connection --- nova/tests/test_metadata.py | 1 + nova/wsgi.py | 29 ----------------------------- 2 files changed, 1 insertion(+), 29 deletions(-) diff --git a/nova/tests/test_metadata.py b/nova/tests/test_metadata.py index c7095914c..c862726ab 100644 --- a/nova/tests/test_metadata.py +++ b/nova/tests/test_metadata.py @@ -31,6 +31,7 @@ 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, diff --git a/nova/wsgi.py b/nova/wsgi.py index 2613357b2..33ba852bc 100644 --- a/nova/wsgi.py +++ b/nova/wsgi.py @@ -91,35 +91,6 @@ class Request(webob.Request): class Application(object): """Base WSGI application wrapper. Subclasses need to implement __call__.""" - @classmethod - def fake_connection(cls): - """Return a fake httplib connection object for this Application""" - class FakeConnection(object): - """Faked version of httplib.HTTPConnection to talk to app.""" - def __init__(self): - self.app = cls() - - def request(self, method, url, body=None, headers={}): - self.req = webob.Request.blank("/" + url.lstrip("/")) - self.req.remote_addr = '127.0.0.1' - self.req.method = method - if headers: - self.req.headers = headers - if body: - self.req.body = body - - def getresponse(self): - res = self.req.get_response(self.app) - - # httplib.Response has a read() method...fake it out - def fake_reader(): - return res.body - - setattr(res, 'read', fake_reader) - return res - - return FakeConnection() - @classmethod def factory(cls, global_config, **local_config): """Used for paste app factories in paste.deploy config files. -- cgit From 6d1ce4a46a9684ac718bfc19dc8a362fbc9a9641 Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Mon, 27 Jun 2011 09:02:22 -0700 Subject: remove extra stuff from clean vlans --- tools/clean-vlans | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 -- cgit From 7230c384d1373e8718bc7fd2fd86b1e805a0573e Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Mon, 27 Jun 2011 09:05:05 -0700 Subject: make nova-debug work with new style instances --- tools/nova-debug | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/tools/nova-debug b/tools/nova-debug index 3ff68ca35..fd9cb054a 100755 --- a/tools/nova-debug +++ b/tools/nova-debug @@ -1,4 +1,3 @@ -#!/usr/bin/env bash # vim: tabstop=4 shiftwidth=4 softtabstop=4 # Copyright 2010 United States Government as represented by the @@ -30,13 +29,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 +67,13 @@ sed -i "s/.*<\/serial>/ Date: Mon, 27 Jun 2011 09:07:49 -0700 Subject: update nova.sh --- contrib/nova.sh | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) 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. -- cgit From b245dfd693107318c90d1af4fdf057ea1a5fc5c2 Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Mon, 27 Jun 2011 09:17:37 -0700 Subject: missed the bin line --- tools/nova-debug | 1 + 1 file changed, 1 insertion(+) diff --git a/tools/nova-debug b/tools/nova-debug index fd9cb054a..0a78af16a 100755 --- a/tools/nova-debug +++ b/tools/nova-debug @@ -1,3 +1,4 @@ +#!/usr/bin/env bash # vim: tabstop=4 shiftwidth=4 softtabstop=4 # Copyright 2010 United States Government as represented by the -- cgit From 62ce69c1c63df818a2a6f1be6cdad33cbe6ef796 Mon Sep 17 00:00:00 2001 From: Sandy Walsh Date: Mon, 27 Jun 2011 20:21:45 -0700 Subject: getting started --- nova/rpc.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/nova/rpc.py b/nova/rpc.py index 2e78a31e7..d4ac19c20 100644 --- a/nova/rpc.py +++ b/nova/rpc.py @@ -45,6 +45,8 @@ from nova import flags from nova import log as logging from nova import utils +from nova.notifier import api as notifier + LOG = logging.getLogger('nova.rpc') @@ -312,6 +314,7 @@ class ConsumerSet(object): if not it: break while True: + ex = None try: it.next() except StopIteration: @@ -319,7 +322,17 @@ class ConsumerSet(object): except greenlet.GreenletExit: running = False break + except exception.NovaException, e: + if not e.notification_level: + ex = e + # We have an exception we can + # tell the Notification system about. + # Pass it on. + except Exception as e: + ex = e + + if ex: LOG.exception(_("Exception while processing consumer")) self.reconnect() # Break to outer loop -- cgit From 4c98425ba7a53c8b966317444abe2d4f7b6556d8 Mon Sep 17 00:00:00 2001 From: Sandy Walsh Date: Tue, 28 Jun 2011 05:51:40 -0700 Subject: refactoring to compute from scheduler --- nova/notifier/api.py | 39 +++++++++++++++++++++++++++++++++++++++ nova/rpc.py | 9 ++------- nova/scheduler/driver.py | 10 ++++++++++ 3 files changed, 51 insertions(+), 7 deletions(-) diff --git a/nova/notifier/api.py b/nova/notifier/api.py index d49517c8b..027aa7cc3 100644 --- a/nova/notifier/api.py +++ b/nova/notifier/api.py @@ -37,6 +37,45 @@ class BadPriorityException(Exception): pass +def publisher_id(service, host=None): + if not host: + host = FLAGS.host + return "%s.%s" % (service, host) + + +def msgkeys(event_type, instance_id, level, publisher_id): + return dict(event_type=event_type, instance_id=instance_id, + notification_level=level, publisher_id=publisher_id) + + +def safe_notify(publisher_id, event_type, priority, payload): + try: + notify(publisher_id, event_type, notification_level, payload) + exception Exception, e: + LOG.exception(_("Problem '%(e)' attempting to " + "send to notification system." % locals())) + + +def instance_safe_notify(publisher_id, event_type, priority, instance_id, + extra_payload=None): + payload = dict(instance_id = instance_id) + if extra_payload: + payload.extend(extra_payload) + safe_notify(publisher_id, event_type, priority, payload) + + +def exception_to_notification(self, ex): + required = ['instance_id', 'publisher_id', 'notification_level', + 'event_type'] + for key in required: + if not (hasattr(ex, key) and ex.key): + return # Doesn't have everything we need. Skip it. + instance_id = ex.instance_id + publisher_id = ex.publisher_id + notification_level = ex.notification_level + event_type = ex.event_type + instance_safe_notify(publisher_id, event_type, priority, instance_id) + def notify(publisher_id, event_type, priority, payload): """ Sends a notification using the specified driver diff --git a/nova/rpc.py b/nova/rpc.py index d4ac19c20..47d63769a 100644 --- a/nova/rpc.py +++ b/nova/rpc.py @@ -44,7 +44,6 @@ from nova import fakerabbit from nova import flags from nova import log as logging from nova import utils - from nova.notifier import api as notifier @@ -323,12 +322,8 @@ class ConsumerSet(object): running = False break except exception.NovaException, e: - if not e.notification_level: - ex = e - # We have an exception we can - # tell the Notification system about. - # Pass it on. - + ex = e + notifier.exception_to_notification(e) except Exception as e: ex = e diff --git a/nova/scheduler/driver.py b/nova/scheduler/driver.py index 0b257c5d8..7e5a15c7f 100644 --- a/nova/scheduler/driver.py +++ b/nova/scheduler/driver.py @@ -27,6 +27,7 @@ from nova import db from nova import exception from nova import flags from nova import log as logging +from nova import notifier from nova import rpc from nova import utils from nova.compute import power_state @@ -47,6 +48,15 @@ class WillNotSchedule(exception.Error): pass +def publisher_id(host=None): + return notifier.publisher_id("scheduler", host) + + +def notifier(instance_id, event_type, level, host=None): + return dict(instance_id=instance_id, event_type=event_type, + notification_level=level, host=publisher_id(host)) + + class Scheduler(object): """The base class that all Scheduler clases should inherit from.""" -- cgit From a0f968235332e5400d507bbafa99bc0728aa8479 Mon Sep 17 00:00:00 2001 From: Sandy Walsh Date: Tue, 28 Jun 2011 21:04:50 -0700 Subject: moved to wrap_exception decorator --- nova/compute/manager.py | 7 ++++++- nova/exception.py | 8 +++++++- nova/notifier/api.py | 26 -------------------------- nova/rpc.py | 8 -------- 4 files changed, 13 insertions(+), 36 deletions(-) diff --git a/nova/compute/manager.py b/nova/compute/manager.py index 5aed2c677..86d375b9e 100644 --- a/nova/compute/manager.py +++ b/nova/compute/manager.py @@ -54,6 +54,7 @@ from nova import utils from nova import volume from nova.compute import power_state from nova.compute.utils import terminate_volumes +from nova.notifier import api as notifier from nova.virt import driver @@ -111,6 +112,10 @@ def checks_instance_lock(function): return decorated_function +def publisher_id(host=None): + return notifier.publisher_id("compute", host) + + class ComputeManager(manager.SchedulerDependentManager): """Manages the running instances from creation to destruction.""" @@ -1158,7 +1163,7 @@ class ComputeManager(manager.SchedulerDependentManager): {"method": "pre_live_migration", "args": {'instance_id': instance_id}}) - except Exception: + except Exception, e: msg = _("Pre live migration for %(i_name)s failed at %(dest)s") LOG.error(msg % locals()) self.recover_live_migration(context, instance_ref) diff --git a/nova/exception.py b/nova/exception.py index f3a452228..4b625dd04 100644 --- a/nova/exception.py +++ b/nova/exception.py @@ -81,11 +81,17 @@ def wrap_db_error(f): _wrap.func_name = f.func_name -def wrap_exception(f): +def wrap_exception(f, notifier=None, publisher_id=None, level=None): def _wrap(*args, **kw): try: return f(*args, **kw) except Exception, e: + if notifier != None and 'safe_notify' in notifier.dir(): + event_type = f.__name__ + payload = dict(args=args, exception=e) + payload.update(kw) + notifier.safe_notify(publisher_id, event_type, level, payload) + if not isinstance(e, Error): #exc_type, exc_value, exc_traceback = sys.exc_info() LOG.exception(_('Uncaught exception')) diff --git a/nova/notifier/api.py b/nova/notifier/api.py index 027aa7cc3..89527be16 100644 --- a/nova/notifier/api.py +++ b/nova/notifier/api.py @@ -43,11 +43,6 @@ def publisher_id(service, host=None): return "%s.%s" % (service, host) -def msgkeys(event_type, instance_id, level, publisher_id): - return dict(event_type=event_type, instance_id=instance_id, - notification_level=level, publisher_id=publisher_id) - - def safe_notify(publisher_id, event_type, priority, payload): try: notify(publisher_id, event_type, notification_level, payload) @@ -55,27 +50,6 @@ def safe_notify(publisher_id, event_type, priority, payload): LOG.exception(_("Problem '%(e)' attempting to " "send to notification system." % locals())) - -def instance_safe_notify(publisher_id, event_type, priority, instance_id, - extra_payload=None): - payload = dict(instance_id = instance_id) - if extra_payload: - payload.extend(extra_payload) - safe_notify(publisher_id, event_type, priority, payload) - - -def exception_to_notification(self, ex): - required = ['instance_id', 'publisher_id', 'notification_level', - 'event_type'] - for key in required: - if not (hasattr(ex, key) and ex.key): - return # Doesn't have everything we need. Skip it. - instance_id = ex.instance_id - publisher_id = ex.publisher_id - notification_level = ex.notification_level - event_type = ex.event_type - instance_safe_notify(publisher_id, event_type, priority, instance_id) - def notify(publisher_id, event_type, priority, payload): """ Sends a notification using the specified driver diff --git a/nova/rpc.py b/nova/rpc.py index 47d63769a..2e78a31e7 100644 --- a/nova/rpc.py +++ b/nova/rpc.py @@ -44,7 +44,6 @@ from nova import fakerabbit from nova import flags from nova import log as logging from nova import utils -from nova.notifier import api as notifier LOG = logging.getLogger('nova.rpc') @@ -313,7 +312,6 @@ class ConsumerSet(object): if not it: break while True: - ex = None try: it.next() except StopIteration: @@ -321,13 +319,7 @@ class ConsumerSet(object): except greenlet.GreenletExit: running = False break - except exception.NovaException, e: - ex = e - notifier.exception_to_notification(e) except Exception as e: - ex = e - - if ex: LOG.exception(_("Exception while processing consumer")) self.reconnect() # Break to outer loop -- cgit From 8bd200505ada97780d3a63927cfadcded456b30d Mon Sep 17 00:00:00 2001 From: Sandy Walsh Date: Wed, 29 Jun 2011 08:14:43 -0700 Subject: moved to wrap_exception approach --- nova/compute/manager.py | 20 ++++++++++---------- nova/exception.py | 6 +++--- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/nova/compute/manager.py b/nova/compute/manager.py index 86d375b9e..f8ec3c861 100644 --- a/nova/compute/manager.py +++ b/nova/compute/manager.py @@ -200,7 +200,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(publisher_id()) def refresh_security_group_rules(self, context, security_group_id, **kwargs): """Tell the virtualization driver to refresh security group rules. @@ -210,7 +210,7 @@ class ComputeManager(manager.SchedulerDependentManager): """ return self.driver.refresh_security_group_rules(security_group_id) - @exception.wrap_exception + @exception.wrap_exception(publisher_id()) def refresh_security_group_members(self, context, security_group_id, **kwargs): """Tell the virtualization driver to refresh security group members. @@ -220,7 +220,7 @@ class ComputeManager(manager.SchedulerDependentManager): """ return self.driver.refresh_security_group_members(security_group_id) - @exception.wrap_exception + @exception.wrap_exception(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() @@ -355,11 +355,11 @@ class ComputeManager(manager.SchedulerDependentManager): # be fixed once we have no-db-messaging pass - @exception.wrap_exception + @exception.wrap_exception(publisher_id()) def run_instance(self, context, instance_id, **kwargs): self._run_instance(context, instance_id, **kwargs) - @exception.wrap_exception + @exception.wrap_exception(publisher_id()) @checks_instance_lock def start_instance(self, context, instance_id): """Starting an instance on this host.""" @@ -421,7 +421,7 @@ class ComputeManager(manager.SchedulerDependentManager): if action_str == 'Terminating': terminate_volumes(self.db, context, instance_id) - @exception.wrap_exception + @exception.wrap_exception(publisher_id()) @checks_instance_lock def terminate_instance(self, context, instance_id): """Terminate an instance on this host.""" @@ -430,14 +430,14 @@ class ComputeManager(manager.SchedulerDependentManager): # TODO(ja): should we keep it in a terminated state for a bit? self.db.instance_destroy(context, instance_id) - @exception.wrap_exception + @exception.wrap_exception(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(publisher_id()) @checks_instance_lock def rebuild_instance(self, context, instance_id, **kwargs): """Destroy and re-make this instance. @@ -466,7 +466,7 @@ class ComputeManager(manager.SchedulerDependentManager): self._update_launched_at(context, instance_id) self._update_state(context, instance_id) - @exception.wrap_exception + @exception.wrap_exception(publisher_id()) @checks_instance_lock def reboot_instance(self, context, instance_id): """Reboot an instance on this host.""" @@ -491,7 +491,7 @@ class ComputeManager(manager.SchedulerDependentManager): self.driver.reboot(instance_ref) self._update_state(context, instance_id) - @exception.wrap_exception + @exception.wrap_exception(publisher_id()) def snapshot_instance(self, context, instance_id, image_id): """Snapshot an instance on this host.""" context = context.elevated() diff --git a/nova/exception.py b/nova/exception.py index 4b625dd04..fb2094c04 100644 --- a/nova/exception.py +++ b/nova/exception.py @@ -25,6 +25,7 @@ SHOULD include dedicated exception logging. """ from nova import log as logging +from nova.notifier import api as notifier LOG = logging.getLogger('nova.exception') @@ -81,13 +82,12 @@ def wrap_db_error(f): _wrap.func_name = f.func_name -def wrap_exception(f, notifier=None, publisher_id=None, level=None): +def wrap_exception(f, event_type=None, publisher_id=None, level=notifier.ERROR): def _wrap(*args, **kw): try: return f(*args, **kw) except Exception, e: - if notifier != None and 'safe_notify' in notifier.dir(): - event_type = f.__name__ + if event_type and publisher_id: payload = dict(args=args, exception=e) payload.update(kw) notifier.safe_notify(publisher_id, event_type, level, payload) -- cgit From 5686488517f702bd4ba714edeea89ea1993ac220 Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Wed, 29 Jun 2011 09:49:49 -0700 Subject: Allow a port name in the server ref for image create --- nova/api/openstack/images.py | 11 ++++++++--- nova/tests/api/openstack/test_images.py | 13 +++++++++++++ 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/nova/api/openstack/images.py b/nova/api/openstack/images.py index d43340e10..22c79e2e9 100644 --- a/nova/api/openstack/images.py +++ b/nova/api/openstack/images.py @@ -208,9 +208,14 @@ 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'): + head, _sep, tail = server_ref.rpartition('/') + + url, _sep, version = req.application_url.rpartition('/') + long_url = '%s:%s/%s' % (url, FLAGS.osapi_port, version) + valid_urls = ['%s/servers' % req.application_url, + '%s/servers' % long_url] + if head and head not in valid_urls: + LOG.warn(head) msg = _("serverRef must match request url") raise webob.exc.HTTPBadRequest(explanation=msg) diff --git a/nova/tests/api/openstack/test_images.py b/nova/tests/api/openstack/test_images.py index 446d68e9e..6c36f96a7 100644 --- a/nova/tests/api/openstack/test_images.py +++ b/nova/tests/api/openstack/test_images.py @@ -1011,6 +1011,19 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): result = json.loads(response.body) self.assertEqual(result['image']['serverRef'], serverRef) + def test_create_image_v1_1_actual_server_ref_port(self): + + serverRef = 'http://localhost:8774/v1.1/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) + self.assertEqual(result['image']['serverRef'], serverRef) + def test_create_image_v1_1_server_ref_bad_hostname(self): serverRef = 'http://asdf/v1.1/servers/1' -- cgit From 799919fe59d4a4faed1ce4effd9705173671e4da Mon Sep 17 00:00:00 2001 From: Sandy Walsh Date: Wed, 29 Jun 2011 19:10:11 -0700 Subject: done and done --- nova/exception.py | 3 ++- nova/tests/test_exception.py | 58 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 1 deletion(-) diff --git a/nova/exception.py b/nova/exception.py index c6d2bbc3d..ea590199c 100644 --- a/nova/exception.py +++ b/nova/exception.py @@ -82,7 +82,8 @@ def wrap_db_error(f): _wrap.func_name = f.func_name -def wrap_exception(notifier=None, publisher_id=None, event_type=None, level=None): +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. diff --git a/nova/tests/test_exception.py b/nova/tests/test_exception.py index 4d3b9cc73..7e30d150f 100644 --- a/nova/tests/test_exception.py +++ b/nova/tests/test_exception.py @@ -32,3 +32,61 @@ 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 safe_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(self): + wrapped = exception.wrap_exception() + self.assertEquals(99, wrapped(good_function)()) + self.assertRaises(exception.Error, wrapped(bad_function_error)) + + # 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) -- cgit From 002b389aa90059c1c1986d4c1a3fbcd38527b4b4 Mon Sep 17 00:00:00 2001 From: Sandy Walsh Date: Wed, 29 Jun 2011 19:11:27 -0700 Subject: pep8 --- nova/tests/test_exception.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nova/tests/test_exception.py b/nova/tests/test_exception.py index 7e30d150f..63c807a84 100644 --- a/nova/tests/test_exception.py +++ b/nova/tests/test_exception.py @@ -49,7 +49,7 @@ class FakeNotifier(object): self.provided_event = event self.provided_priority = priority self.provided_payload = payload - + def good_function(): return 99 -- cgit From d17171b4277337388b372459571d9f3904798bca Mon Sep 17 00:00:00 2001 From: Sandy Walsh Date: Wed, 29 Jun 2011 19:19:40 -0700 Subject: pep8 --- nova/tests/test_exception.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nova/tests/test_exception.py b/nova/tests/test_exception.py index 63c807a84..16e273f89 100644 --- a/nova/tests/test_exception.py +++ b/nova/tests/test_exception.py @@ -71,7 +71,7 @@ class WrapExceptionTestCase(test.TestCase): # 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", -- cgit From fbe296a7ab3bf1b9ee2bf765d13b5675ff4d6295 Mon Sep 17 00:00:00 2001 From: Sandy Walsh Date: Wed, 29 Jun 2011 20:02:30 -0700 Subject: clean up --- nova/api/openstack/wsgi.py | 1 - nova/compute/manager.py | 1 - nova/scheduler/driver.py | 5 ----- 3 files changed, 7 deletions(-) diff --git a/nova/api/openstack/wsgi.py b/nova/api/openstack/wsgi.py index 836a13b62..5b6e3cb1d 100644 --- a/nova/api/openstack/wsgi.py +++ b/nova/api/openstack/wsgi.py @@ -394,5 +394,4 @@ class Resource(wsgi.Application): """Find action-spefic method on controller and call it.""" controller_method = getattr(self.controller, action) - print "DISPATCHING", self.controller, action return controller_method(req=request, **action_args) diff --git a/nova/compute/manager.py b/nova/compute/manager.py index 78c98cf42..d5530c296 100644 --- a/nova/compute/manager.py +++ b/nova/compute/manager.py @@ -1183,7 +1183,6 @@ class ComputeManager(manager.SchedulerDependentManager): :returns: See driver.update_available_resource() """ - print "UPDATE AVAILABLE" return self.driver.update_available_resource(context, self.host) def pre_live_migration(self, context, instance_id, time=None): diff --git a/nova/scheduler/driver.py b/nova/scheduler/driver.py index db392a607..ec2c9ef78 100644 --- a/nova/scheduler/driver.py +++ b/nova/scheduler/driver.py @@ -30,7 +30,6 @@ from nova import log as logging from nova import rpc from nova import utils from nova.compute import power_state -from nova.notifier import api as notifier_api FLAGS = flags.FLAGS @@ -49,10 +48,6 @@ class WillNotSchedule(exception.Error): pass -def publisher_id(host=None): - return notifier_api.publisher_id("scheduler", host) - - class Scheduler(object): """The base class that all Scheduler clases should inherit from.""" -- cgit From 302d5ad4caaef4f02f475c2e9abd782ac630beb8 Mon Sep 17 00:00:00 2001 From: Joseph Suh Date: Thu, 30 Jun 2011 14:24:27 -0400 Subject: fix bug 800759 --- nova/scheduler/zone_manager.py | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/nova/scheduler/zone_manager.py b/nova/scheduler/zone_manager.py index ba7403c15..e0daa01e2 100644 --- a/nova/scheduler/zone_manager.py +++ b/nova/scheduler/zone_manager.py @@ -109,6 +109,7 @@ class ZoneManager(object): self.last_zone_db_check = datetime.datetime.min self.zone_states = {} # { : ZoneState } self.service_states = {} # { : { : { cap k : v }}} + self.service_time_stamp = {} # reported time self.green_pool = greenpool.GreenPool() def get_zone_list(self): @@ -125,14 +126,18 @@ class ZoneManager(object): # But it's likely to change once we understand what the Best-Match # code will need better. combined = {} # { _ : (min, max), ... } + allowed_time_diff = FLAGS.periodic_interval * 3 for host, host_dict in hosts_dict.iteritems(): - for service_name, service_dict in host_dict.iteritems(): - for cap, value in service_dict.iteritems(): - 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) + if (utils.utcnow() - self.service_time_stamp[host]) <= \ + datetime.timedelta(seconds=allowed_time_diff): + for service_name, service_dict in host_dict.iteritems(): + for cap, value in service_dict.iteritems(): + 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) return combined @@ -174,3 +179,4 @@ class ZoneManager(object): service_caps = self.service_states.get(host, {}) service_caps[service_name] = capabilities self.service_states[host] = service_caps + self.service_time_stamp[host] = utils.utcnow() -- cgit From 7142aa734c1e77a6efacac5bff918b44240b7a1a Mon Sep 17 00:00:00 2001 From: "matt.dietz@rackspace.com" <> Date: Thu, 30 Jun 2011 16:49:03 -0500 Subject: Merge from trunk --- bin/nova-ajax-console-proxy | 0 bin/nova-api | 0 bin/nova-compute | 0 bin/nova-console | 0 bin/nova-dhcpbridge | 0 bin/nova-direct-api | 0 bin/nova-import-canonical-imagestore | 0 bin/nova-instancemonitor | 0 bin/nova-manage | 0 bin/nova-network | 0 bin/nova-objectstore | 0 bin/nova-scheduler | 0 bin/nova-vncproxy | 0 bin/nova-volume | 0 bin/stack | 0 doc/source/devref/down.sh | 0 doc/source/devref/up.sh | 0 nova/cloudpipe/bootscript.template | 0 plugins/xenserver/networking/etc/init.d/host-rules | 0 plugins/xenserver/networking/etc/init.d/openvswitch-nova | 0 .../networking/etc/xensource/scripts/ovs_configure_base_flows.py | 0 .../xenserver/networking/etc/xensource/scripts/ovs_configure_vif_flows.py | 0 plugins/xenserver/networking/etc/xensource/scripts/vif_rules.py | 0 plugins/xenserver/xenapi/etc/xapi.d/plugins/agent | 0 plugins/xenserver/xenapi/etc/xapi.d/plugins/pluginlib_nova.py | 0 plugins/xenserver/xenapi/etc/xapi.d/plugins/xenstore.py | 0 tools/ajaxterm/ajaxterm.py | 0 tools/ajaxterm/configure | 0 tools/clean-vlans | 0 tools/euca-get-ajax-console | 0 tools/nova-debug | 0 31 files changed, 0 insertions(+), 0 deletions(-) mode change 100755 => 100644 bin/nova-ajax-console-proxy mode change 100755 => 100644 bin/nova-api mode change 100755 => 100644 bin/nova-compute mode change 100755 => 100644 bin/nova-console mode change 100755 => 100644 bin/nova-dhcpbridge mode change 100755 => 100644 bin/nova-direct-api mode change 100755 => 100644 bin/nova-import-canonical-imagestore mode change 100755 => 100644 bin/nova-instancemonitor mode change 100755 => 100644 bin/nova-manage mode change 100755 => 100644 bin/nova-network mode change 100755 => 100644 bin/nova-objectstore mode change 100755 => 100644 bin/nova-scheduler mode change 100755 => 100644 bin/nova-vncproxy mode change 100755 => 100644 bin/nova-volume mode change 100755 => 100644 bin/stack mode change 100644 => 100755 doc/source/devref/down.sh mode change 100644 => 100755 doc/source/devref/up.sh mode change 100755 => 100644 nova/cloudpipe/bootscript.template mode change 100755 => 100644 plugins/xenserver/networking/etc/init.d/host-rules mode change 100755 => 100644 plugins/xenserver/networking/etc/init.d/openvswitch-nova mode change 100755 => 100644 plugins/xenserver/networking/etc/xensource/scripts/ovs_configure_base_flows.py mode change 100755 => 100644 plugins/xenserver/networking/etc/xensource/scripts/ovs_configure_vif_flows.py mode change 100755 => 100644 plugins/xenserver/networking/etc/xensource/scripts/vif_rules.py mode change 100755 => 100644 plugins/xenserver/xenapi/etc/xapi.d/plugins/agent mode change 100755 => 100644 plugins/xenserver/xenapi/etc/xapi.d/plugins/pluginlib_nova.py mode change 100755 => 100644 plugins/xenserver/xenapi/etc/xapi.d/plugins/xenstore.py mode change 100755 => 100644 tools/ajaxterm/ajaxterm.py mode change 100755 => 100644 tools/ajaxterm/configure mode change 100755 => 100644 tools/clean-vlans mode change 100755 => 100644 tools/euca-get-ajax-console mode change 100755 => 100644 tools/nova-debug diff --git a/bin/nova-ajax-console-proxy b/bin/nova-ajax-console-proxy old mode 100755 new mode 100644 diff --git a/bin/nova-api b/bin/nova-api old mode 100755 new mode 100644 diff --git a/bin/nova-compute b/bin/nova-compute old mode 100755 new mode 100644 diff --git a/bin/nova-console b/bin/nova-console old mode 100755 new mode 100644 diff --git a/bin/nova-dhcpbridge b/bin/nova-dhcpbridge old mode 100755 new mode 100644 diff --git a/bin/nova-direct-api b/bin/nova-direct-api old mode 100755 new mode 100644 diff --git a/bin/nova-import-canonical-imagestore b/bin/nova-import-canonical-imagestore old mode 100755 new mode 100644 diff --git a/bin/nova-instancemonitor b/bin/nova-instancemonitor old mode 100755 new mode 100644 diff --git a/bin/nova-manage b/bin/nova-manage old mode 100755 new mode 100644 diff --git a/bin/nova-network b/bin/nova-network old mode 100755 new mode 100644 diff --git a/bin/nova-objectstore b/bin/nova-objectstore old mode 100755 new mode 100644 diff --git a/bin/nova-scheduler b/bin/nova-scheduler old mode 100755 new mode 100644 diff --git a/bin/nova-vncproxy b/bin/nova-vncproxy old mode 100755 new mode 100644 diff --git a/bin/nova-volume b/bin/nova-volume old mode 100755 new mode 100644 diff --git a/bin/stack b/bin/stack old mode 100755 new mode 100644 diff --git a/doc/source/devref/down.sh b/doc/source/devref/down.sh old mode 100644 new mode 100755 diff --git a/doc/source/devref/up.sh b/doc/source/devref/up.sh old mode 100644 new mode 100755 diff --git a/nova/cloudpipe/bootscript.template b/nova/cloudpipe/bootscript.template old mode 100755 new mode 100644 diff --git a/plugins/xenserver/networking/etc/init.d/host-rules b/plugins/xenserver/networking/etc/init.d/host-rules old mode 100755 new mode 100644 diff --git a/plugins/xenserver/networking/etc/init.d/openvswitch-nova b/plugins/xenserver/networking/etc/init.d/openvswitch-nova old mode 100755 new mode 100644 diff --git a/plugins/xenserver/networking/etc/xensource/scripts/ovs_configure_base_flows.py b/plugins/xenserver/networking/etc/xensource/scripts/ovs_configure_base_flows.py old mode 100755 new mode 100644 diff --git a/plugins/xenserver/networking/etc/xensource/scripts/ovs_configure_vif_flows.py b/plugins/xenserver/networking/etc/xensource/scripts/ovs_configure_vif_flows.py old mode 100755 new mode 100644 diff --git a/plugins/xenserver/networking/etc/xensource/scripts/vif_rules.py b/plugins/xenserver/networking/etc/xensource/scripts/vif_rules.py old mode 100755 new mode 100644 diff --git a/plugins/xenserver/xenapi/etc/xapi.d/plugins/agent b/plugins/xenserver/xenapi/etc/xapi.d/plugins/agent old mode 100755 new mode 100644 diff --git a/plugins/xenserver/xenapi/etc/xapi.d/plugins/pluginlib_nova.py b/plugins/xenserver/xenapi/etc/xapi.d/plugins/pluginlib_nova.py old mode 100755 new mode 100644 diff --git a/plugins/xenserver/xenapi/etc/xapi.d/plugins/xenstore.py b/plugins/xenserver/xenapi/etc/xapi.d/plugins/xenstore.py old mode 100755 new mode 100644 diff --git a/tools/ajaxterm/ajaxterm.py b/tools/ajaxterm/ajaxterm.py old mode 100755 new mode 100644 diff --git a/tools/ajaxterm/configure b/tools/ajaxterm/configure old mode 100755 new mode 100644 diff --git a/tools/clean-vlans b/tools/clean-vlans old mode 100755 new mode 100644 diff --git a/tools/euca-get-ajax-console b/tools/euca-get-ajax-console old mode 100755 new mode 100644 diff --git a/tools/nova-debug b/tools/nova-debug old mode 100755 new mode 100644 -- cgit From e789dd29c48ee8ad2b10eeb9ff24725f0e696bed Mon Sep 17 00:00:00 2001 From: Sandy Walsh Date: Fri, 1 Jul 2011 07:31:17 -0700 Subject: review fixes --- nova/exception.py | 4 ++-- nova/notifier/api.py | 14 +++++--------- nova/tests/test_exception.py | 7 ++++++- 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/nova/exception.py b/nova/exception.py index ea590199c..6e277b68d 100644 --- a/nova/exception.py +++ b/nova/exception.py @@ -113,8 +113,8 @@ def wrap_exception(notifier=None, publisher_id=None, event_type=None, # propagated. temp_type = f.__name__ - notifier.safe_notify(publisher_id, temp_type, temp_level, - payload) + notifier.notify(publisher_id, temp_type, temp_level, + payload) if not isinstance(e, Error): #exc_type, exc_value, exc_traceback = sys.exc_info() diff --git a/nova/notifier/api.py b/nova/notifier/api.py index d388eda96..98969fd3e 100644 --- a/nova/notifier/api.py +++ b/nova/notifier/api.py @@ -45,14 +45,6 @@ def publisher_id(service, host=None): return "%s.%s" % (service, host) -def safe_notify(publisher_id, event_type, priority, payload): - try: - notify(publisher_id, event_type, notification_level, payload) - except Exception, e: - LOG.exception(_("Problem '%(e)s' attempting to " - "send to notification system." % locals())) - - def notify(publisher_id, event_type, priority, payload): """ Sends a notification using the specified driver @@ -95,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/tests/test_exception.py b/nova/tests/test_exception.py index 16e273f89..692b714e5 100644 --- a/nova/tests/test_exception.py +++ b/nova/tests/test_exception.py @@ -64,11 +64,16 @@ def bad_function_exception(): class WrapExceptionTestCase(test.TestCase): - def test_wrap_exception(self): + 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)) -- cgit From 81716e8142fac86e779514997335999df4375a34 Mon Sep 17 00:00:00 2001 From: Sandy Walsh Date: Fri, 1 Jul 2011 07:38:17 -0700 Subject: tweak --- nova/tests/test_exception.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nova/tests/test_exception.py b/nova/tests/test_exception.py index 692b714e5..cd74f8871 100644 --- a/nova/tests/test_exception.py +++ b/nova/tests/test_exception.py @@ -44,7 +44,7 @@ class FakeNotifier(object): self.provided_priority = None self.provided_payload = None - def safe_notify(self, publisher, event, priority, payload): + def notify(self, publisher, event, priority, payload): self.provided_publisher = publisher self.provided_event = event self.provided_priority = priority -- cgit From 72bdbc314ac311e8c831410bc4f9c8935bf9d5e8 Mon Sep 17 00:00:00 2001 From: Sandy Walsh Date: Fri, 1 Jul 2011 08:09:19 -0700 Subject: trunk merge --- nova/compute/manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nova/compute/manager.py b/nova/compute/manager.py index 016915d2f..cf42a39aa 100644 --- a/nova/compute/manager.py +++ b/nova/compute/manager.py @@ -842,7 +842,7 @@ 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 pause_instance(self, context, instance_id): """Pause an instance on this host.""" -- cgit From 401bbecb4fbb819d2b1daa3ee1bebcd6460742a1 Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Fri, 1 Jul 2011 11:12:13 -0700 Subject: use url parse instead of manually splitting --- nova/api/openstack/images.py | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/nova/api/openstack/images.py b/nova/api/openstack/images.py index 22c79e2e9..9717f3175 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 @@ -22,7 +23,6 @@ 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.views import images as images_view @@ -208,18 +208,24 @@ class ControllerV11(Controller): msg = _("Expected serverRef attribute on server entity.") raise webob.exc.HTTPBadRequest(explanation=msg) - head, _sep, tail = server_ref.rpartition('/') - - url, _sep, version = req.application_url.rpartition('/') - long_url = '%s:%s/%s' % (url, FLAGS.osapi_port, version) - valid_urls = ['%s/servers' % req.application_url, - '%s/servers' % long_url] - if head and head not in valid_urls: - LOG.warn(head) + 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) + LOG.warn(locals()) + 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'] -- cgit From a9b0dbb8dcd708a46af58f61bab39b0bc9e8a6e8 Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Fri, 1 Jul 2011 12:03:27 -0700 Subject: remove logging statement --- nova/api/openstack/images.py | 1 - 1 file changed, 1 deletion(-) diff --git a/nova/api/openstack/images.py b/nova/api/openstack/images.py index 9717f3175..b4ab0b3af 100644 --- a/nova/api/openstack/images.py +++ b/nova/api/openstack/images.py @@ -220,7 +220,6 @@ class ControllerV11(Controller): host_ok = passed.hostname == expected.hostname port_ok = (passed.port == expected.port or passed.port == FLAGS.osapi_port) - LOG.warn(locals()) 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) -- cgit From 4227264153e06d576387f76b267f3d35ff17f391 Mon Sep 17 00:00:00 2001 From: Mike Scherbakov Date: Sat, 2 Jul 2011 01:28:13 +0400 Subject: Improvements to nova-manage: network list now includes vlan and projectID, added servers list filtered by zone if needed --- bin/nova-manage | 43 +++++++++++++++++++++++++++++++++++-------- 1 file changed, 35 insertions(+), 8 deletions(-) diff --git a/bin/nova-manage b/bin/nova-manage index 7dfe91698..8ba78a56a 100755 --- a/bin/nova-manage +++ b/bin/nova-manage @@ -615,15 +615,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""" @@ -812,6 +816,28 @@ class ServiceCommands(object): {"method": "update_available_resource"}) +class ServerCommands(object): + """List servers""" + + def list(self, zone=None): + """Show a list of all servers. 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] + servers = [] + for srv in services: + if not [s for s in servers if s['host'] == srv['host']]: + servers.append(srv) + + for srv in servers: + print "%-25s\t%-15s" % (srv['host'], srv['availability_zone']) + + class DbCommands(object): """Class for managing the database.""" @@ -1191,6 +1217,7 @@ CATEGORIES = [ ('project', ProjectCommands), ('role', RoleCommands), ('service', ServiceCommands), + ('server', ServerCommands), ('shell', ShellCommands), ('user', UserCommands), ('version', VersionCommands), -- cgit From 0834f3d64b2cc37407c24a9b717e218d758adf79 Mon Sep 17 00:00:00 2001 From: Brian Waldon Date: Mon, 4 Jul 2011 15:14:36 -0400 Subject: properly displays addresses in each network, not just public/private; adding addresses attribute to server entities --- nova/api/openstack/__init__.py | 11 +-- nova/api/openstack/ips.py | 79 ++++++++++++++---- nova/api/openstack/servers.py | 31 ++++--- nova/api/openstack/views/addresses.py | 38 +++++++-- nova/api/openstack/views/servers.py | 13 ++- nova/db/sqlalchemy/api.py | 23 +++-- nova/tests/api/openstack/test_servers.py | 139 ++++++++++++++++++++++++++++--- 7 files changed, 272 insertions(+), 62 deletions(-) diff --git a/nova/api/openstack/__init__.py b/nova/api/openstack/__init__.py index f24017df0..3e4d87ee0 100644 --- a/nova/api/openstack/__init__.py +++ b/nova/api/openstack/__init__.py @@ -144,9 +144,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,8 +154,8 @@ 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'), + mapper.resource("ip", "ips", controller=ips.create_resource('1.0'), + member=dict(public='GET', private='GET'), parent_resource=dict(member_name='server', collection_name='servers')) @@ -177,3 +174,7 @@ class APIRouterV11(APIRouter): controller=server_metadata.create_resource(), parent_resource=dict(member_name='server', collection_name='servers')) + + mapper.resource("ip", "ips", controller=ips.create_resource('1.1'), + parent_resource=dict(member_name='server', + collection_name='servers')) diff --git a/nova/api/openstack/ips.py b/nova/api/openstack/ips.py index 71646b6d3..b83e2f9b3 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 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'}, @@ -72,7 +121,7 @@ def create_resource(): serializers = { 'application/xml': wsgi.XMLDictSerializer(metadata=metadata, - xmlns=wsgi.XMLNS_V10), + xmlns=xmlns), } - return wsgi.Resource(Controller(), serializers=serializers) + return wsgi.Resource(controller, serializers=serializers) diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index fc1ab8d46..3c29c2606 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,8 +99,7 @@ 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()) @@ -130,8 +129,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 @@ -418,10 +416,10 @@ class ControllerV10(Controller): 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) @@ -481,16 +479,23 @@ 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) + context = req.environ['nova.context'] + interfaces = db.api.virtual_interface_get_by_instance(context, + instance['id']) + instance['virtual_interfaces'] = interfaces + + 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 diff --git a/nova/api/openstack/views/addresses.py b/nova/api/openstack/views/addresses.py index b59eb4751..c7464a7c5 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,30 @@ 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] = [] + + for fixed_ip in interface['fixed_ips']: + ip = {'addr': fixed_ip['address'], 'version': 4} + networks[network_label].append(ip) + return networks + + def build_network(self, interfaces, network_label): + for interface in interfaces: + if interface['network']['label'] == network_label: + ips = self._extract_fixed_ips(interface) + return {network_label: ips} + return None + + def _extract_fixed_ips(self, interface): + fixed_ips = [] + for fixed_ip in interface['fixed_ips']: + ip = {'addr': fixed_ip['address'], 'version': 4} + fixed_ips.append(ip) + return fixed_ips + diff --git a/nova/api/openstack/views/servers.py b/nova/api/openstack/views/servers.py index cbfa5aae7..691cc48ca 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['virtual_interfaces'] + response['addresses'] = self.addresses_builder.build(interfaces) + def _build_extra(self, response, inst): self._build_links(response, inst) diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index ffd009513..854b7dea7 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -118,6 +118,20 @@ 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) + return wrapper + + + ################### @require_admin_context @@ -921,6 +935,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. @@ -3071,14 +3086,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/tests/api/openstack/test_servers.py b/nova/tests/api/openstack/test_servers.py index c3ca1431b..6ee87830a 100644 --- a/nova/tests/api/openstack/test_servers.py +++ b/nova/tests/api/openstack/test_servers.py @@ -65,6 +65,12 @@ 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_server_with_addresses(private, public): def _return_server(context, id): return stub_instance(id, private_address=private, @@ -417,23 +423,134 @@ 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) - self.stubs.Set(nova.db.api, 'instance_get', new_return_server) + 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'}, + ], + }, + ] + + _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') 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'}, + {'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': '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'}, + {'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': '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_list(self): req = webob.Request.blank('/v1.0/servers') -- cgit From c2bb27363e5155adb9ba36844586d390ddd14de4 Mon Sep 17 00:00:00 2001 From: Sandy Walsh Date: Tue, 5 Jul 2011 12:16:35 -0700 Subject: fixed zone id check --- nova/scheduler/zone_aware_scheduler.py | 2 +- nova/tests/scheduler/test_zone_aware_scheduler.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/nova/scheduler/zone_aware_scheduler.py b/nova/scheduler/zone_aware_scheduler.py index 1cc98e48b..84ebd6b2f 100644 --- a/nova/scheduler/zone_aware_scheduler.py +++ b/nova/scheduler/zone_aware_scheduler.py @@ -183,7 +183,7 @@ class ZoneAwareScheduler(driver.Scheduler): continue for zone_rec in zones: - if zone_rec['api_url'] != zone: + if zone_rec['id'] != zone: continue for item in result: 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'), -- cgit From 46690df48392c8967fc4f0ea05b5dba152fa400a Mon Sep 17 00:00:00 2001 From: Sandy Walsh Date: Tue, 5 Jul 2011 13:24:31 -0700 Subject: copy paste --- nova/compute/api.py | 10 ++++++++-- nova/compute/manager.py | 14 ++++++++++++++ nova/network/api.py | 8 ++++++++ nova/network/manager.py | 7 +++++++ 4 files changed, 37 insertions(+), 2 deletions(-) diff --git a/nova/compute/api.py b/nova/compute/api.py index 28459dc75..a17ab2e1c 100644 --- a/nova/compute/api.py +++ b/nova/compute/api.py @@ -889,8 +889,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, network_id) + + @scheduler_api.reroute_compute("remove_fixed_ip") + def remove_fixed_ip(self, context, instance_id, network_id): + """Remove fixed_ip from specified network to given instance.""" + self._cast_compute_message('remove_fixed_ip_from_instance', context, + instance_id, network_id) + #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 bbbddde0a..0f761c939 100644 --- a/nova/compute/manager.py +++ b/nova/compute/manager.py @@ -840,6 +840,20 @@ class ComputeManager(manager.SchedulerDependentManager): self.inject_network_info(context, instance_id) self.reset_network(context, instance_id) + @exception.wrap_exception + @checks_instance_lock + def remove_fixed_ip_from_instance(self, context, instance_id, network_id): + """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, + network_id) + self.inject_network_info(context, instance_id) + self.reset_network(context, instance_id) + + @exception.wrap_exception @checks_instance_lock def pause_instance(self, context, instance_id): diff --git a/nova/network/api.py b/nova/network/api.py index b2b96082b..0a70ff73e 100644 --- a/nova/network/api.py +++ b/nova/network/api.py @@ -156,6 +156,14 @@ class API(base.Base): {'method': 'add_fixed_ip_to_instance', 'args': args}) + def remove_fixed_ip_from_instance(self, context, instance_id, network_id): + """Removes a fixed ip from instance from specified network.""" + args = {'instance_id': instance_id, + 'network_id': network_id} + 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 d42bc8c4e..675101b5a 100644 --- a/nova/network/manager.py +++ b/nova/network/manager.py @@ -480,6 +480,13 @@ class NetworkManager(manager.SchedulerDependentManager): networks = [self.db.network_get(context, network_id)] self._allocate_fixed_ips(context, instance_id, networks) + def remove_fixed_ip_from_instance(self, context, instance_id, network_id): + """Removes a fixed ip from an instance from specified network.""" + networks = [self.db.network_get(context, network_id)] + # TODO(sandy): Do the right thing here ... + x = 1+1 # pep8 to catch this. + self._allocate_fixed_ips(context, instance_id, networks) + 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 -- cgit From f34952f27aa7acdb8bb617346aba281a86e918ae Mon Sep 17 00:00:00 2001 From: Sandy Walsh Date: Wed, 6 Jul 2011 08:14:58 -0700 Subject: slightly more fleshed out call path --- nova/compute/api.py | 4 ++-- nova/compute/manager.py | 7 +++++-- nova/network/api.py | 9 +++++++-- nova/network/manager.py | 31 +++++++++++++++++++++++-------- 4 files changed, 37 insertions(+), 14 deletions(-) diff --git a/nova/compute/api.py b/nova/compute/api.py index a17ab2e1c..a07ab4435 100644 --- a/nova/compute/api.py +++ b/nova/compute/api.py @@ -892,10 +892,10 @@ class API(base.Base): instance_id, network_id) @scheduler_api.reroute_compute("remove_fixed_ip") - def remove_fixed_ip(self, context, instance_id, network_id): + def remove_fixed_ip(self, context, instance_id, network_id, ip): """Remove fixed_ip from specified network to given instance.""" self._cast_compute_message('remove_fixed_ip_from_instance', context, - instance_id, network_id) + instance_id, network_id, ip) #TODO(tr3buchet): how to run this in the correct zone? diff --git a/nova/compute/manager.py b/nova/compute/manager.py index 0f761c939..ab5499209 100644 --- a/nova/compute/manager.py +++ b/nova/compute/manager.py @@ -840,16 +840,19 @@ class ComputeManager(manager.SchedulerDependentManager): self.inject_network_info(context, instance_id) self.reset_network(context, instance_id) + + # TODO(sandy) pep8 until checked ... @exception.wrap_exception @checks_instance_lock - def remove_fixed_ip_from_instance(self, context, instance_id, network_id): + def remove_fixed_ip_from_instance(self, context, instance_id, network_id, + ip): """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, - network_id) + network_id, ip) self.inject_network_info(context, instance_id) self.reset_network(context, instance_id) diff --git a/nova/network/api.py b/nova/network/api.py index 0a70ff73e..54514f482 100644 --- a/nova/network/api.py +++ b/nova/network/api.py @@ -156,14 +156,19 @@ class API(base.Base): {'method': 'add_fixed_ip_to_instance', 'args': args}) - def remove_fixed_ip_from_instance(self, context, instance_id, network_id): + + # TODO(sandy) pep8 until checked + def remove_fixed_ip_from_instance(self, context, instance_id, network_id, + ip): """Removes a fixed ip from instance from specified network.""" args = {'instance_id': instance_id, - 'network_id': network_id} + 'network_id': network_id, + 'ip': ip} 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 675101b5a..2ae050b14 100644 --- a/nova/network/manager.py +++ b/nova/network/manager.py @@ -480,12 +480,23 @@ class NetworkManager(manager.SchedulerDependentManager): networks = [self.db.network_get(context, network_id)] self._allocate_fixed_ips(context, instance_id, networks) - def remove_fixed_ip_from_instance(self, context, instance_id, network_id): + + #TODO(sandy) - PEP8 until this is checked ... + def remove_fixed_ip_from_instance(self, context, instance_id, network_id, + ip): """Removes a fixed ip from an instance from specified network.""" networks = [self.db.network_get(context, network_id)] - # TODO(sandy): Do the right thing here ... - x = 1+1 # pep8 to catch this. - self._allocate_fixed_ips(context, instance_id, networks) + # Find the network that contains this IP ... + network = None + for n in networks: + if ip in n: + network = n + break + if not network: + raise exception.InvalidIP(ip=ip) + + self.deallocate_fixed_ips(context, ip) + def allocate_fixed_ip(self, context, instance_id, network, **kwargs): """Gets a fixed ip from the pool.""" @@ -696,10 +707,14 @@ class FlatManager(NetworkManager): for network in networks: self.allocate_fixed_ip(context, instance_id, network) - def deallocate_fixed_ip(self, context, address, **kwargs): - """Returns a fixed ip to the pool.""" - super(FlatManager, self).deallocate_fixed_ip(context, address, - **kwargs) + + # TODO(sandy): Switched to multi-IP support + def deallocate_fixed_ip(self, context, networks, **kwargs): + """Returns a list of fixed ips to the pool.""" + for network in networks: + address = network['address'] + super(FlatManager, self).deallocate_fixed_ip(context, address, + **kwargs) self.db.fixed_ip_disassociate(context, address) def setup_compute_network(self, context, instance_id): -- cgit From 479b55aefadd88e59a6cd489a39df53fecc46bdf Mon Sep 17 00:00:00 2001 From: Alex Meade Date: Wed, 6 Jul 2011 13:19:37 -0400 Subject: updated expected xml in images show test to represent current spec --- nova/tests/api/openstack/test_images.py | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/nova/tests/api/openstack/test_images.py b/nova/tests/api/openstack/test_images.py index 1e046531c..9291997bc 100644 --- a/nova/tests/api/openstack/test_images.py +++ b/nova/tests/api/openstack/test_images.py @@ -1161,7 +1161,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() @@ -1181,7 +1183,6 @@ class ImageXMLSerializationTest(test.TestCase): { 'href': self.IMAGE_HREF % (1,), 'rel': 'bookmark', - 'type': 'application/json', }, ], }, @@ -1191,24 +1192,27 @@ class ImageXMLSerializationTest(test.TestCase): actual = minidom.parseString(output.replace(" ", "")) 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_now = self.TIMESTAMP expected = minidom.parseString(""" - - - + progress="80"> + + + + + + - - value1 - + value1 """.replace(" ", "") % (locals())) -- cgit From 94a6af26e46d4df35294ad0bf4dc4883b7bf052e Mon Sep 17 00:00:00 2001 From: Alex Meade Date: Wed, 6 Jul 2011 14:47:41 -0400 Subject: Further test update and begin correcting serialization --- nova/api/openstack/images.py | 5 +++-- nova/tests/api/openstack/test_images.py | 30 ++++++++++++++++++++++++++---- 2 files changed, 29 insertions(+), 6 deletions(-) diff --git a/nova/api/openstack/images.py b/nova/api/openstack/images.py index bde9507c8..2e3d4f157 100644 --- a/nova/api/openstack/images.py +++ b/nova/api/openstack/images.py @@ -267,8 +267,9 @@ class ImageXMLSerializer(wsgi.XMLDictSerializer): metadata = { "attributes": { "image": ["id", "name", "updated", "created", "status", - "serverId", "progress", "serverRef"], - "link": ["rel", "type", "href"], + "serverId", "progress"], + "link": ["rel", "href"], + "server": ["name", "id"], }, } diff --git a/nova/tests/api/openstack/test_images.py b/nova/tests/api/openstack/test_images.py index 9291997bc..4f00ac1f8 100644 --- a/nova/tests/api/openstack/test_images.py +++ b/nova/tests/api/openstack/test_images.py @@ -1168,14 +1168,29 @@ class ImageXMLSerializationTest(test.TestCase): def test_show(self): serializer = images.ImageXMLSerializer() + #so we can see the full diff in the output + self.maxDiff = None fixture = { 'image': { 'id': 1, 'name': 'Image1', 'created': self.TIMESTAMP, 'updated': self.TIMESTAMP, - 'serverRef': self.SERVER_HREF, 'status': 'ACTIVE', + 'server': { + 'id': 1, + 'name': 'Server1', + 'links': [ + { + 'href': self.SERVER_BOOKMARK, + 'rel': 'bookmark', + }, + { + 'href': self.SERVER_HREF, + 'rel': 'self', + }, + ], + }, 'metadata': { 'key1': 'value1', }, @@ -1184,6 +1199,10 @@ class ImageXMLSerializationTest(test.TestCase): 'href': self.IMAGE_HREF % (1,), 'rel': 'bookmark', }, + { + 'href': self.IMAGE_BOOKMARK % (1,), + 'rel': 'self', + }, ], }, } @@ -1192,7 +1211,7 @@ class ImageXMLSerializationTest(test.TestCase): actual = minidom.parseString(output.replace(" ", "")) expected_server_href = self.SERVER_HREF - expected_server_bookmark = self.SERVER_BOOKMARK_ + expected_server_bookmark = self.SERVER_BOOKMARK expected_href = self.IMAGE_HREF % (1, ) expected_bookmark = self.IMAGE_BOOKMARK % (1, ) expected_now = self.TIMESTAMP @@ -1205,18 +1224,21 @@ class ImageXMLSerializationTest(test.TestCase): created="%(expected_now)s" status="ACTIVE" progress="80"> - + - value1 + + value1 + """.replace(" ", "") % (locals())) + print actual.toxml() self.assertEqual(expected.toxml(), actual.toxml()) def test_show_zero_metadata(self): -- cgit From bb09e0e1bc1c587e7677eb5db68a8fbd293ecd5b Mon Sep 17 00:00:00 2001 From: Brian Waldon Date: Wed, 6 Jul 2011 16:28:10 -0400 Subject: first round --- nova/api/openstack/accounts.py | 6 +- nova/api/openstack/backup_schedules.py | 13 +- nova/api/openstack/consoles.py | 12 +- nova/api/openstack/contrib/floating_ips.py | 4 +- nova/api/openstack/create_instance_helper.py | 2 +- nova/api/openstack/flavors.py | 6 +- nova/api/openstack/image_metadata.py | 5 +- nova/api/openstack/images.py | 6 +- nova/api/openstack/ips.py | 5 +- nova/api/openstack/limits.py | 6 +- nova/api/openstack/server_metadata.py | 6 +- nova/api/openstack/servers.py | 10 +- nova/api/openstack/shared_ip_groups.py | 12 +- nova/api/openstack/users.py | 6 +- nova/api/openstack/versions.py | 5 +- nova/api/openstack/wsgi.py | 144 +++++++++++++-------- nova/api/openstack/zones.py | 9 +- .../api/openstack/contrib/test_floating_ips.py | 3 + nova/tests/api/openstack/test_servers.py | 46 +++---- nova/tests/api/openstack/test_wsgi.py | 61 +++++---- nova/tests/integrated/api/client.py | 7 +- 21 files changed, 217 insertions(+), 157 deletions(-) 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/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/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..178374721 100644 --- a/nova/api/openstack/images.py +++ b/nova/api/openstack/images.py @@ -338,8 +338,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..23e5432d6 100644 --- a/nova/api/openstack/ips.py +++ b/nova/api/openstack/ips.py @@ -70,9 +70,10 @@ def create_resource(): }, } - serializers = { + body_serializers = { 'application/xml': wsgi.XMLDictSerializer(metadata=metadata, xmlns=wsgi.XMLNS_V10), } + 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..d08287f6b 100644 --- a/nova/api/openstack/limits.py +++ b/nova/api/openstack/limits.py @@ -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): 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 fc1ab8d46..9953a8fb7 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -616,14 +616,16 @@ def create_resource(version='1.0'): '1.1': wsgi.XMLNS_V11, }[version] - serializers = { + 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) + 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/wsgi.py b/nova/api/openstack/wsgi.py index 5b6e3cb1d..6726531be 100644 --- a/nova/api/openstack/wsgi.py +++ b/nova/api/openstack/wsgi.py @@ -46,7 +46,7 @@ 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 @@ -67,17 +67,20 @@ class TextDeserializer(object): def default(self, datastring): """Default deserialization code should live here""" - raise NotImplementedError() + return {} class JSONDeserializer(TextDeserializer): - def default(self, datastring): + 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 +93,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 +124,27 @@ class XMLDeserializer(TextDeserializer): listnames) return result + def default(self, datastring): + return {'body': self._from_xml(datastring)} + + +class RequestHeadersDeserializer(object): + def deserialize(self, request, action): + 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 +158,45 @@ 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() + + if content_type is None: + LOG.debug(_("No Content-Type provided in request")) + return {} + + if 'Content-Length' not in request.headers: + msg = _("No Content-Length provided in request") + LOG.debug(msg) + return {} + + if not request.content_length > 0: + msg = _("Request has Content-Length of zero") + LOG.debug(msg) + return {} + + deserializer = self.get_body_deserializer(content_type) + + except exception.InvalidContentType: + LOG.debug(_("Unable to read body as provided Content-Type")) + raise + + return deserializer.deserialize(request.body, action) + + def get_body_deserializer(self, content_type): try: - return self.deserializers[content_type] + return self.body_deserializers[content_type] except (KeyError, TypeError): raise exception.InvalidContentType(content_type=content_type) @@ -295,19 +323,23 @@ class XMLDictSerializer(DictSerializer): return result +class ResponseHeadersSerializer(object): + def serialize(self, response, data, action): + 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 +349,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 +380,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 +401,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,7 +409,7 @@ 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: @@ -394,4 +432,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/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/test_servers.py b/nova/tests/api/openstack/test_servers.py index c3ca1431b..5004fbce1 100644 --- a/nova/tests/api/openstack/test_servers.py +++ b/nova/tests/api/openstack/test_servers.py @@ -917,7 +917,7 @@ class ServersTest(test.TestCase): 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 """ @@ -1603,7 +1603,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 = """ @@ -1618,7 +1618,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 = """ @@ -1633,7 +1633,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 = """ @@ -1650,7 +1650,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 = """ @@ -1667,7 +1667,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 = """ @@ -1679,7 +1679,7 @@ class TestServerCreateRequestXMLDeserializer(unittest.TestCase): """ 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 = """ @@ -1690,7 +1690,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 = """ @@ -1705,7 +1705,7 @@ class TestServerCreateRequestXMLDeserializer(unittest.TestCase): """ 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 = """ @@ -1714,7 +1714,7 @@ class TestServerCreateRequestXMLDeserializer(unittest.TestCase): aabbccdd""" 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 = """ @@ -1723,7 +1723,7 @@ class TestServerCreateRequestXMLDeserializer(unittest.TestCase): """ 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 = """ @@ -1732,7 +1732,7 @@ class TestServerCreateRequestXMLDeserializer(unittest.TestCase): """ 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 = """ @@ -1744,7 +1744,7 @@ class TestServerCreateRequestXMLDeserializer(unittest.TestCase): """ 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 = """ @@ -1757,7 +1757,7 @@ class TestServerCreateRequestXMLDeserializer(unittest.TestCase): """ 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 = """ @@ -1769,7 +1769,7 @@ class TestServerCreateRequestXMLDeserializer(unittest.TestCase): """ 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 = """ @@ -1782,7 +1782,7 @@ class TestServerCreateRequestXMLDeserializer(unittest.TestCase): """ 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 = """ @@ -1794,7 +1794,7 @@ class TestServerCreateRequestXMLDeserializer(unittest.TestCase): """ 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 = """ @@ -1807,7 +1807,7 @@ class TestServerCreateRequestXMLDeserializer(unittest.TestCase): """ 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 = """ @@ -1820,7 +1820,7 @@ class TestServerCreateRequestXMLDeserializer(unittest.TestCase): """ 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 = """ @@ -1866,7 +1866,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 = """ @@ -1876,9 +1876,9 @@ b25zLiINCg0KLVJpY2hhcmQgQmFjaA==""", flavorRef="http://localhost:8774/v1.1/flavors/1"> """ 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") @@ -1943,7 +1943,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..938637207 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 = "" - 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') @@ -144,12 +143,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,19 +167,24 @@ class XMLDeserializerTest(test.TestCase): 1 """.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 = """""" - as_dict = {"a": {}} + as_dict = {"body": {"a": {}}} deserializer = wsgi.XMLDeserializer() self.assertEqual(deserializer.deserialize(xml), as_dict) @@ -190,23 +199,24 @@ class ResponseSerializerTest(test.TestCase): def serialize(self, data, action='default'): return 'pew_xml' - self.serializers = { + self.body_serializers = { 'application/json': JSONSerializer(), 'application/XML': XMLSerializer(), } - self.serializer = wsgi.ResponseSerializer(serializers=self.serializers) + self.serializer = wsgi.ResponseSerializer(self.body_serializers) 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): @@ -230,24 +240,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/integrated/api/client.py b/nova/tests/integrated/api/client.py index 76c03c5fa..3d255b40c 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 -- cgit From 0415c413872697c6f9fecc28928af0525780f868 Mon Sep 17 00:00:00 2001 From: Mark Washenberger Date: Wed, 6 Jul 2011 16:40:00 -0400 Subject: correct test_show --- nova/api/openstack/images.py | 70 ++++++++++++++++++++++----------- nova/tests/api/openstack/test_images.py | 23 ++++++----- 2 files changed, 61 insertions(+), 32 deletions(-) diff --git a/nova/api/openstack/images.py b/nova/api/openstack/images.py index 2e3d4f157..750b034b9 100644 --- a/nova/api/openstack/images.py +++ b/nova/api/openstack/images.py @@ -262,34 +262,60 @@ class ControllerV11(Controller): return {'instance_ref': server_ref} -class ImageXMLSerializer(wsgi.XMLDictSerializer): +class ImageXMLSerializer(wsgi.DictSerializer): - metadata = { - "attributes": { - "image": ["id", "name", "updated", "created", "status", - "serverId", "progress"], - "link": ["rel", "href"], - "server": ["name", "id"], - }, - } - - xmlns = wsgi.XMLNS_V11 + xmlns = {'': wsgi.XMLNS_V11, 'atom': "http://www.w3.org/2005/Atom"} def __init__(self): self.metadata_serializer = image_metadata.ImageMetadataXMLSerializer() + def _add_xmlns(self, node): + for key, ns in self.xmlns.iteritems(): + if key is not '': + name = 'xmlns:%s' % key + else: + name = 'xmlns' + node.setAttribute(name, ns) + def _image_to_xml(self, xml_doc, image): - try: - metadata = image.pop('metadata').items() - except Exception: - LOG.debug(_("Image object missing metadata attribute")) - metadata = {} + image_node = xml_doc.createElement('image') + self._add_image_attributes(image_node, image) - 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 + server_node = self._create_server_node(xml_doc, image['server']) + image_node.appendChild(server_node) + + metadata = image.get('metadata', {}) + metadata_node = self._create_metadata_node(xml_doc, metadata.items()) + image_node.appendChild(metadata_node) + + self._add_atom_links(xml_doc, image_node, image['links']) + + 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']) + node.setAttribute('progress', str(image['progress'])) + + def _create_server_node(self, xml_doc, server): + server_node = xml_doc.createElement('server') + server_node.setAttribute('id', str(server['id'])) + server_node.setAttribute('name', server['name']) + self._add_atom_links(xml_doc, server_node, server['links']) + return server_node + + def _create_metadata_node(self, xml_doc, metadata): + return self.metadata_serializer.meta_list_to_xml(xml_doc, metadata) + + def _add_atom_links(self, xml_doc, node, links): + for link in links: + link_node = xml_doc.createElement('atom:link') + link_node.setAttribute('rel', link['rel']) + link_node.setAttribute('href', link['href']) + node.appendChild(link_node) def _image_list_to_xml(self, xml_doc, images): container_node = xml_doc.createElement('images') @@ -307,7 +333,7 @@ class ImageXMLSerializer(wsgi.XMLDictSerializer): def _image_list_to_xml_string(self, images): xml_doc = minidom.Document() container_node = self._image_list_to_xml(xml_doc, images) - self._add_xmlns(container_node) + self._add_xmlns(item_node) return container_node.toprettyxml(indent=' ') def detail(self, images_dict): diff --git a/nova/tests/api/openstack/test_images.py b/nova/tests/api/openstack/test_images.py index 4f00ac1f8..873607997 100644 --- a/nova/tests/api/openstack/test_images.py +++ b/nova/tests/api/openstack/test_images.py @@ -1177,18 +1177,19 @@ class ImageXMLSerializationTest(test.TestCase): 'created': self.TIMESTAMP, 'updated': self.TIMESTAMP, 'status': 'ACTIVE', + 'progress': 80, 'server': { 'id': 1, 'name': 'Server1', 'links': [ - { - 'href': self.SERVER_BOOKMARK, - 'rel': 'bookmark', - }, { 'href': self.SERVER_HREF, 'rel': 'self', }, + { + 'href': self.SERVER_BOOKMARK, + 'rel': 'bookmark', + }, ], }, 'metadata': { @@ -1197,11 +1198,11 @@ class ImageXMLSerializationTest(test.TestCase): 'links': [ { 'href': self.IMAGE_HREF % (1,), - 'rel': 'bookmark', + 'rel': 'self', }, { 'href': self.IMAGE_BOOKMARK % (1,), - 'rel': 'self', + 'rel': 'bookmark', }, ], }, @@ -1225,19 +1226,21 @@ class ImageXMLSerializationTest(test.TestCase): status="ACTIVE" progress="80"> - - + + - - value1 + + """.replace(" ", "") % (locals())) + print expected.toxml() + print '---' print actual.toxml() self.assertEqual(expected.toxml(), actual.toxml()) -- cgit From bd297ae3bc779853cf82f05d0d4da60305416a99 Mon Sep 17 00:00:00 2001 From: Sandy Walsh Date: Wed, 6 Jul 2011 13:43:48 -0700 Subject: changed calling signature to be (instance_id, address) --- nova/compute/api.py | 4 ++-- nova/compute/manager.py | 6 ++---- nova/network/api.py | 6 ++---- nova/network/manager.py | 32 +++++++++++--------------------- 4 files changed, 17 insertions(+), 31 deletions(-) diff --git a/nova/compute/api.py b/nova/compute/api.py index a07ab4435..e4992bbfa 100644 --- a/nova/compute/api.py +++ b/nova/compute/api.py @@ -892,10 +892,10 @@ class API(base.Base): instance_id, network_id) @scheduler_api.reroute_compute("remove_fixed_ip") - def remove_fixed_ip(self, context, instance_id, network_id, 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, network_id, ip) + instance_id, address) #TODO(tr3buchet): how to run this in the correct zone? diff --git a/nova/compute/manager.py b/nova/compute/manager.py index ab5499209..b9a3d1a5e 100644 --- a/nova/compute/manager.py +++ b/nova/compute/manager.py @@ -844,15 +844,13 @@ class ComputeManager(manager.SchedulerDependentManager): # TODO(sandy) pep8 until checked ... @exception.wrap_exception @checks_instance_lock - def remove_fixed_ip_from_instance(self, context, instance_id, network_id, - ip): + 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, - network_id, ip) + address) self.inject_network_info(context, instance_id) self.reset_network(context, instance_id) diff --git a/nova/network/api.py b/nova/network/api.py index 54514f482..bcd69d52b 100644 --- a/nova/network/api.py +++ b/nova/network/api.py @@ -158,12 +158,10 @@ class API(base.Base): # TODO(sandy) pep8 until checked - def remove_fixed_ip_from_instance(self, context, instance_id, network_id, - ip): + 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, - 'network_id': network_id, - 'ip': ip} + 'address': address} rpc.cast(context, FLAGS.network_topic, {'method': 'remove_fixed_ip_from_instance', 'args': args}) diff --git a/nova/network/manager.py b/nova/network/manager.py index 2ae050b14..4b9fb98f4 100644 --- a/nova/network/manager.py +++ b/nova/network/manager.py @@ -482,20 +482,14 @@ class NetworkManager(manager.SchedulerDependentManager): #TODO(sandy) - PEP8 until this is checked ... - def remove_fixed_ip_from_instance(self, context, instance_id, network_id, - ip): + def remove_fixed_ip_from_instance(self, context, instance_id, address): """Removes a fixed ip from an instance from specified network.""" - networks = [self.db.network_get(context, network_id)] - # Find the network that contains this IP ... - network = None - for n in networks: - if ip in n: - network = n - break - if not network: - raise exception.InvalidIP(ip=ip) - - self.deallocate_fixed_ips(context, ip) + addresses = self.db.fixed_ip_get_by_instance(context, instance_id) + for addr in addresses: + if addr == address: + self.deallocate_fixed_ip(context, address) + return + raise exception.NoSuchAddress(address=address) def allocate_fixed_ip(self, context, instance_id, network, **kwargs): @@ -707,14 +701,10 @@ class FlatManager(NetworkManager): for network in networks: self.allocate_fixed_ip(context, instance_id, network) - - # TODO(sandy): Switched to multi-IP support - def deallocate_fixed_ip(self, context, networks, **kwargs): - """Returns a list of fixed ips to the pool.""" - for network in networks: - address = network['address'] - super(FlatManager, self).deallocate_fixed_ip(context, address, - **kwargs) + def deallocate_fixed_ip(self, context, address, **kwargs): + """Returns a fixed ip to the pool.""" + super(FlatManager, self).deallocate_fixed_ip(context, address, + **kwargs) self.db.fixed_ip_disassociate(context, address) def setup_compute_network(self, context, instance_id): -- cgit From 8caf69dc93d9112e9be8989cd2136a407e09df44 Mon Sep 17 00:00:00 2001 From: Mark Washenberger Date: Thu, 7 Jul 2011 09:24:40 -0400 Subject: progress and server are optional --- nova/api/openstack/images.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/nova/api/openstack/images.py b/nova/api/openstack/images.py index 750b034b9..eaa7aef5a 100644 --- a/nova/api/openstack/images.py +++ b/nova/api/openstack/images.py @@ -281,8 +281,9 @@ class ImageXMLSerializer(wsgi.DictSerializer): image_node = xml_doc.createElement('image') self._add_image_attributes(image_node, image) - server_node = self._create_server_node(xml_doc, image['server']) - image_node.appendChild(server_node) + if 'server' in image: + server_node = self._create_server_node(xml_doc, image['server']) + image_node.appendChild(server_node) metadata = image.get('metadata', {}) metadata_node = self._create_metadata_node(xml_doc, metadata.items()) @@ -298,7 +299,8 @@ class ImageXMLSerializer(wsgi.DictSerializer): node.setAttribute('created', image['created']) node.setAttribute('updated', image['updated']) node.setAttribute('status', image['status']) - node.setAttribute('progress', str(image['progress'])) + if 'progress' in image: + node.setAttribute('progress', str(image['progress'])) def _create_server_node(self, xml_doc, server): server_node = xml_doc.createElement('server') @@ -333,7 +335,7 @@ class ImageXMLSerializer(wsgi.DictSerializer): def _image_list_to_xml_string(self, images): xml_doc = minidom.Document() container_node = self._image_list_to_xml(xml_doc, images) - self._add_xmlns(item_node) + self._add_xmlns(container_node) return container_node.toprettyxml(indent=' ') def detail(self, images_dict): -- cgit From d24a8689dceaae1145d0cc0aa12e60bfdabbe2b2 Mon Sep 17 00:00:00 2001 From: Sandy Walsh Date: Thu, 7 Jul 2011 06:25:03 -0700 Subject: unit tests --- nova/compute/api.py | 3 ++- nova/network/manager.py | 4 ++-- nova/tests/test_network.py | 33 +++++++++++++++++++++++++++++++++ 3 files changed, 37 insertions(+), 3 deletions(-) diff --git a/nova/compute/api.py b/nova/compute/api.py index e4992bbfa..8507c5871 100644 --- a/nova/compute/api.py +++ b/nova/compute/api.py @@ -890,7 +890,8 @@ class API(base.Base): """Add fixed_ip from specified network to given instance.""" self._cast_compute_message('add_fixed_ip_to_instance', context, instance_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.""" diff --git a/nova/network/manager.py b/nova/network/manager.py index 4b9fb98f4..ea946a1f4 100644 --- a/nova/network/manager.py +++ b/nova/network/manager.py @@ -486,10 +486,10 @@ class NetworkManager(manager.SchedulerDependentManager): """Removes a fixed ip from an instance from specified network.""" addresses = self.db.fixed_ip_get_by_instance(context, instance_id) for addr in addresses: - if addr == address: + if addr['address'] == address: self.deallocate_fixed_ip(context, address) return - raise exception.NoSuchAddress(address=address) + raise exception.FixedIpNotFound(id=address) def allocate_fixed_ip(self, context, instance_id, network, **kwargs): diff --git a/nova/tests/test_network.py b/nova/tests/test_network.py index 6d5166019..d5cab47cf 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 @@ -238,3 +239,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.FixedIpNotFound, + manager.remove_fixed_ip_from_instance, + None, 99, 'bad input') -- cgit From b50e92d43f958bf966fce4f608daa467b40453c1 Mon Sep 17 00:00:00 2001 From: Mark Washenberger Date: Thu, 7 Jul 2011 10:48:17 -0400 Subject: make server and image metadata optional --- nova/api/openstack/images.py | 7 +- nova/tests/api/openstack/test_images.py | 158 +++++++++++++++++++++++++------- 2 files changed, 128 insertions(+), 37 deletions(-) diff --git a/nova/api/openstack/images.py b/nova/api/openstack/images.py index eaa7aef5a..8746db4e0 100644 --- a/nova/api/openstack/images.py +++ b/nova/api/openstack/images.py @@ -285,9 +285,10 @@ class ImageXMLSerializer(wsgi.DictSerializer): server_node = self._create_server_node(xml_doc, image['server']) image_node.appendChild(server_node) - metadata = image.get('metadata', {}) - metadata_node = self._create_metadata_node(xml_doc, metadata.items()) - image_node.appendChild(metadata_node) + metadata = image.get('metadata', {}).items() + if len(metadata) > 0: + metadata_node = self._create_metadata_node(xml_doc, metadata) + image_node.appendChild(metadata_node) self._add_atom_links(xml_doc, image_node, image['links']) diff --git a/nova/tests/api/openstack/test_images.py b/nova/tests/api/openstack/test_images.py index 873607997..25c7001f5 100644 --- a/nova/tests/api/openstack/test_images.py +++ b/nova/tests/api/openstack/test_images.py @@ -1197,11 +1197,11 @@ class ImageXMLSerializationTest(test.TestCase): }, 'links': [ { - 'href': self.IMAGE_HREF % (1,), + 'href': self.IMAGE_HREF % 1, 'rel': 'self', }, { - 'href': self.IMAGE_BOOKMARK % (1,), + 'href': self.IMAGE_BOOKMARK % 1, 'rel': 'bookmark', }, ], @@ -1213,8 +1213,8 @@ class ImageXMLSerializationTest(test.TestCase): 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 = self.IMAGE_HREF % 1 + expected_bookmark = self.IMAGE_BOOKMARK % 1 expected_now = self.TIMESTAMP expected = minidom.parseString(""" """.replace(" ", "") % (locals())) - print expected.toxml() - print '---' - print actual.toxml() self.assertEqual(expected.toxml(), actual.toxml()) def test_show_zero_metadata(self): serializer = images.ImageXMLSerializer() + self.maxDiff = None fixture = { 'image': { 'id': 1, 'name': 'Image1', 'created': self.TIMESTAMP, 'updated': self.TIMESTAMP, - 'serverRef': self.SERVER_HREF, 'status': 'ACTIVE', + 'server': { + 'id': 1, + 'name': 'Server1', + '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', }, ], }, @@ -1270,21 +1284,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(""" - - - - + status="ACTIVE"> + + + + + + """.replace(" ", "") % (locals())) @@ -1293,22 +1310,38 @@ class ImageXMLSerializationTest(test.TestCase): def test_show_image_no_metadata_key(self): serializer = images.ImageXMLSerializer() + self.maxDiff = None fixture = { 'image': { 'id': 1, 'name': 'Image1', 'created': self.TIMESTAMP, 'updated': self.TIMESTAMP, - 'serverRef': self.SERVER_HREF, 'status': 'ACTIVE', + 'server': { + 'id': 1, + 'name': 'Server1', + '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', }, ], - }, } @@ -1316,21 +1349,78 @@ 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(""" - - - - + status="ACTIVE"> + + + + + + + + """.replace(" ", "") % (locals())) + + self.assertEqual(expected.toxml(), actual.toxml()) + + def test_show_no_server(self): + serializer = images.ImageXMLSerializer() + + #so we can see the full diff in the output + self.maxDiff = None + 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(""" + + + + value1 + + + + """.replace(" ", "") % (locals())) @@ -1528,7 +1618,7 @@ class ImageXMLSerializationTest(test.TestCase): }, 'links': [ { - 'href': self.IMAGE_HREF % (1,), + 'href': self.IMAGE_HREF % 1, 'rel': 'bookmark', 'type': 'application/json', }, @@ -1540,7 +1630,7 @@ class ImageXMLSerializationTest(test.TestCase): actual = minidom.parseString(output.replace(" ", "")) expected_server_href = self.SERVER_HREF - expected_href = self.IMAGE_HREF % (1, ) + expected_href = self.IMAGE_HREF % 1 expected_now = self.TIMESTAMP expected = minidom.parseString(""" Date: Thu, 7 Jul 2011 11:06:35 -0400 Subject: Updated test_detail --- nova/tests/api/openstack/test_images.py | 117 +++++++++++++++++++------------- 1 file changed, 69 insertions(+), 48 deletions(-) diff --git a/nova/tests/api/openstack/test_images.py b/nova/tests/api/openstack/test_images.py index 701d8be8a..3e8d4700d 100644 --- a/nova/tests/api/openstack/test_images.py +++ b/nova/tests/api/openstack/test_images.py @@ -1473,84 +1473,105 @@ class ImageXMLSerializationTest(test.TestCase): def test_detail(self): serializer = images.ImageXMLSerializer() - fixtures = { + #so we can see the full diff in the output + self.maxDiff = None + 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, + 'name': 'Server1', + '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(""" - - - - - - - - value2 - - - value1 - - - - - - - - - + + + + + + + + + + + + + value1 + + + + + """.replace(" ", "") % (locals())) -- cgit From 63d579523985cbd4c896d9a05e523761e1cadb3a Mon Sep 17 00:00:00 2001 From: Alex Meade Date: Thu, 7 Jul 2011 11:11:40 -0400 Subject: fixed image create response test --- nova/tests/api/openstack/test_images.py | 44 +++++++++++++++++++++++++-------- 1 file changed, 34 insertions(+), 10 deletions(-) diff --git a/nova/tests/api/openstack/test_images.py b/nova/tests/api/openstack/test_images.py index 3e8d4700d..c8e7df096 100644 --- a/nova/tests/api/openstack/test_images.py +++ b/nova/tests/api/openstack/test_images.py @@ -1580,22 +1580,41 @@ class ImageXMLSerializationTest(test.TestCase): def test_create(self): serializer = images.ImageXMLSerializer() + #so we can see the full diff in the output + self.maxDiff = None fixture = { 'image': { 'id': 1, 'name': 'Image1', 'created': self.TIMESTAMP, 'updated': self.TIMESTAMP, - 'serverRef': self.SERVER_HREF, - 'status': 'ACTIVE', + 'status': 'SAVING', + 'progress': 80, + 'server': { + 'id': 1, + 'name': 'Server1', + 'links': [ + { + 'href': self.SERVER_HREF, + 'rel': 'self', + }, + { + 'href': self.SERVER_BOOKMARK, + 'rel': 'bookmark', + }, + ], + }, 'metadata': { 'key1': 'value1', }, 'links': [ { 'href': self.IMAGE_HREF % 1, + 'rel': 'self', + }, + { + 'href': self.IMAGE_BOOKMARK % 1, 'rel': 'bookmark', - 'type': 'application/json', }, ], }, @@ -1605,25 +1624,30 @@ class ImageXMLSerializationTest(test.TestCase): actual = minidom.parseString(output.replace(" ", "")) 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_now = self.TIMESTAMP expected = minidom.parseString(""" - - - + status="SAVING" + progress="80"> + + + + value1 + + """.replace(" ", "") % (locals())) -- cgit From 7048530fb6083ab92fa6b7a2b5a73fc2bf30709f Mon Sep 17 00:00:00 2001 From: Devendra Modium Date: Thu, 7 Jul 2011 11:25:45 -0400 Subject: Comments Incorporated for Bug800759 --- Authors | 1 + nova/scheduler/zone_manager.py | 55 ++++++++++++++----- nova/tests/test_zones.py | 121 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 163 insertions(+), 14 deletions(-) diff --git a/Authors b/Authors index c3a65f1b4..67cd38bcf 100644 --- a/Authors +++ b/Authors @@ -93,3 +93,4 @@ Yoshiaki Tamura Youcef Laribi Yuriy Taraday Zhixue Wu +Devendra Modium diff --git a/nova/scheduler/zone_manager.py b/nova/scheduler/zone_manager.py index e0daa01e2..2a0816118 100644 --- a/nova/scheduler/zone_manager.py +++ b/nova/scheduler/zone_manager.py @@ -109,7 +109,6 @@ class ZoneManager(object): self.last_zone_db_check = datetime.datetime.min self.zone_states = {} # { : ZoneState } self.service_states = {} # { : { : { cap k : v }}} - self.service_time_stamp = {} # reported time self.green_pool = greenpool.GreenPool() def get_zone_list(self): @@ -126,19 +125,28 @@ class ZoneManager(object): # But it's likely to change once we understand what the Best-Match # code will need better. combined = {} # { _ : (min, max), ... } - allowed_time_diff = FLAGS.periodic_interval * 3 + stale_host_services = {} # { host1 : [svc1, svc2], host2 :[svc1]} for host, host_dict in hosts_dict.iteritems(): - if (utils.utcnow() - self.service_time_stamp[host]) <= \ - datetime.timedelta(seconds=allowed_time_diff): - for service_name, service_dict in host_dict.iteritems(): - for cap, value in service_dict.iteritems(): - 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) - + for service_name, service_dict in host_dict.iteritems(): + + #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): @@ -177,6 +185,25 @@ 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 - self.service_time_stamp[host] = utils.utcnow() + + 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/tests/test_zones.py b/nova/tests/test_zones.py index e132809dc..f4661a328 100644 --- a/nova/tests/test_zones.py +++ b/nova/tests/test_zones.py @@ -198,3 +198,124 @@ 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(self): + zm = zone_manager.ZoneManager() + expiry_time = (FLAGS.periodic_interval * 3) + 1 + + # 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")) + + # Both services became stale + 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")) + + # One service became stale + utils.clear_time_override() + 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(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"]) + + # Delete all services in a host + 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) + + # 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(self): + zm = zone_manager.ZoneManager() + expiry_time = (FLAGS.periodic_interval * 3) + 1 + + # 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))) + + # Service capabilities stale + time_future = utils.utcnow() + datetime.timedelta(seconds=expiry_time) + utils.set_time_override(time_future) + caps = zm.get_zone_capabilities(None) + self.assertEquals(caps, {}) + + # Both host service capabilities recent + utils.clear_time_override() + 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))) + + # One host service capabilities become stale + 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))) + + # Multiple services per host + zm.update_service_capabilities("svc1", "host1", dict(a=1, b=2)) + 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))) + + # Two host services among four become stale + 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))) + + # Three host services among four become stale + zm.update_service_capabilities("svc2", "host1", dict(a=5, b=6)) + zm.update_service_capabilities("svc1", "host2", dict(a=3, b=4)) + 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))) + + # All the host services become stale + zm.update_service_capabilities("svc2", "host1", dict(a=5, b=6)) + zm.update_service_capabilities("svc1", "host2", dict(a=3, b=4)) + zm.update_service_capabilities("svc2", "host2", dict(a=7, b=8)) + utils.set_time_override(time_future) + caps = zm.get_zone_capabilities(None) + self.assertEquals(caps, {}) + -- cgit From ddc33b8163423c5138b40885cb3430104896c676 Mon Sep 17 00:00:00 2001 From: Alex Meade Date: Thu, 7 Jul 2011 11:36:45 -0400 Subject: Added image index --- nova/api/openstack/images.py | 41 ++++++++++++++----- nova/tests/api/openstack/test_images.py | 71 +++++++++++++-------------------- 2 files changed, 60 insertions(+), 52 deletions(-) diff --git a/nova/api/openstack/images.py b/nova/api/openstack/images.py index 8746db4e0..a81dd4299 100644 --- a/nova/api/openstack/images.py +++ b/nova/api/openstack/images.py @@ -278,6 +278,13 @@ class ImageXMLSerializer(wsgi.DictSerializer): node.setAttribute(name, ns) def _image_to_xml(self, xml_doc, image): + image_node = xml_doc.createElement('image') + image_node.setAttribute('id', str(image['id'])) + image_node.setAttribute('name', image['name']) + self._add_atom_links(xml_doc, image_node, image['links']) + 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) @@ -320,33 +327,49 @@ class ImageXMLSerializer(wsgi.DictSerializer): link_node.setAttribute('href', link['href']) node.appendChild(link_node) - def _image_list_to_xml(self, xml_doc, images): + def _image_list_to_xml(self, xml_doc, images, image_to_xml): container_node = xml_doc.createElement('images') 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): + def _image_to_xml_string(self, image, detailed): xml_doc = minidom.Document() - item_node = self._image_to_xml(xml_doc, image) + if detailed: + image_to_xml = self._image_to_xml_detailed + else: + image_to_xml = self._image_to_xml + item_node = 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 _image_list_to_xml_string(self, images, detailed): xml_doc = minidom.Document() - container_node = self._image_list_to_xml(xml_doc, images) + if detailed: + image_to_xml = self._image_to_xml_detailed + else: + image_to_xml = self._image_to_xml + container_node = self._image_list_to_xml(xml_doc, images, image_to_xml) + self._add_xmlns(container_node) return container_node.toprettyxml(indent=' ') + def index(self, images_dict): + return self._image_list_to_xml_string(images_dict['images'], + detailed=False) + def detail(self, images_dict): - return self._image_list_to_xml_string(images_dict['images']) + return self._image_list_to_xml_string(images_dict['images'], + detailed=True) def show(self, image_dict): - return self._image_to_xml_string(image_dict['image']) + return self._image_to_xml_string(image_dict['image'], + detailed=True) def create(self, image_dict): - return self._image_to_xml_string(image_dict['image']) + return self._image_to_xml_string(image_dict['image'], + detailed=True) def create_resource(version='1.0'): diff --git a/nova/tests/api/openstack/test_images.py b/nova/tests/api/openstack/test_images.py index c8e7df096..cf68f5f53 100644 --- a/nova/tests/api/openstack/test_images.py +++ b/nova/tests/api/openstack/test_images.py @@ -1383,70 +1383,53 @@ class ImageXMLSerializationTest(test.TestCase): def test_index(self): serializer = images.ImageXMLSerializer() - fixtures = { + #so we can see the full diff in the output + self.maxDiff = None + 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(""" - - - - - - - - - - - + + + + + + + """.replace(" ", "") % (locals())) @@ -1465,7 +1448,9 @@ class ImageXMLSerializationTest(test.TestCase): expected_serverRef = self.SERVER_HREF expected_now = self.TIMESTAMP expected = minidom.parseString(""" - + """.replace(" ", "") % (locals())) self.assertEqual(expected.toxml(), actual.toxml()) -- cgit From f2b107981c9b37043fe6c68e92cb170b2b701a18 Mon Sep 17 00:00:00 2001 From: Sandy Walsh Date: Thu, 7 Jul 2011 09:45:00 -0700 Subject: cleanup --- nova/compute/api.py | 2 -- nova/compute/manager.py | 3 --- nova/network/api.py | 3 --- nova/network/manager.py | 3 --- 4 files changed, 11 deletions(-) diff --git a/nova/compute/api.py b/nova/compute/api.py index 8507c5871..3e5188ef0 100644 --- a/nova/compute/api.py +++ b/nova/compute/api.py @@ -891,14 +891,12 @@ class API(base.Base): self._cast_compute_message('add_fixed_ip_to_instance', context, instance_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, address) - #TODO(tr3buchet): how to run this in the correct zone? def add_network_to_project(self, context, project_id): """Force adds a network to the project.""" diff --git a/nova/compute/manager.py b/nova/compute/manager.py index b9a3d1a5e..bec2d6568 100644 --- a/nova/compute/manager.py +++ b/nova/compute/manager.py @@ -840,8 +840,6 @@ class ComputeManager(manager.SchedulerDependentManager): self.inject_network_info(context, instance_id) self.reset_network(context, instance_id) - - # TODO(sandy) pep8 until checked ... @exception.wrap_exception @checks_instance_lock def remove_fixed_ip_from_instance(self, context, instance_id, address): @@ -854,7 +852,6 @@ class ComputeManager(manager.SchedulerDependentManager): self.inject_network_info(context, instance_id) self.reset_network(context, instance_id) - @exception.wrap_exception @checks_instance_lock def pause_instance(self, context, instance_id): diff --git a/nova/network/api.py b/nova/network/api.py index bcd69d52b..70b1099f0 100644 --- a/nova/network/api.py +++ b/nova/network/api.py @@ -156,8 +156,6 @@ class API(base.Base): {'method': 'add_fixed_ip_to_instance', 'args': args}) - - # TODO(sandy) pep8 until checked 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, @@ -166,7 +164,6 @@ class API(base.Base): {'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 ea946a1f4..0432179dd 100644 --- a/nova/network/manager.py +++ b/nova/network/manager.py @@ -480,8 +480,6 @@ class NetworkManager(manager.SchedulerDependentManager): networks = [self.db.network_get(context, network_id)] self._allocate_fixed_ips(context, instance_id, networks) - - #TODO(sandy) - PEP8 until this is checked ... def remove_fixed_ip_from_instance(self, context, instance_id, address): """Removes a fixed ip from an instance from specified network.""" addresses = self.db.fixed_ip_get_by_instance(context, instance_id) @@ -491,7 +489,6 @@ class NetworkManager(manager.SchedulerDependentManager): return raise exception.FixedIpNotFound(id=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 -- cgit From 49683f2f84a9eb4436c63465d11ae8f451265eae Mon Sep 17 00:00:00 2001 From: Alex Meade Date: Thu, 7 Jul 2011 14:08:57 -0400 Subject: Dried up images XML serialization --- nova/api/openstack/images.py | 80 ++++++++++++++++++------------------------- nova/api/openstack/servers.py | 12 +++++++ nova/api/openstack/wsgi.py | 12 +++++++ 3 files changed, 58 insertions(+), 46 deletions(-) diff --git a/nova/api/openstack/images.py b/nova/api/openstack/images.py index a81dd4299..bc7517c40 100644 --- a/nova/api/openstack/images.py +++ b/nova/api/openstack/images.py @@ -27,6 +27,7 @@ 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 @@ -262,26 +263,19 @@ class ControllerV11(Controller): return {'instance_ref': server_ref} -class ImageXMLSerializer(wsgi.DictSerializer): +class ImageXMLSerializer(wsgi.XMLDictSerializer): - xmlns = {'': wsgi.XMLNS_V11, 'atom': "http://www.w3.org/2005/Atom"} + xmlns = wsgi.XMLNS_V11 def __init__(self): self.metadata_serializer = image_metadata.ImageMetadataXMLSerializer() - - def _add_xmlns(self, node): - for key, ns in self.xmlns.iteritems(): - if key is not '': - name = 'xmlns:%s' % key - else: - name = 'xmlns' - node.setAttribute(name, ns) + self.server_serializer = servers.ServerXMLSerializer() def _image_to_xml(self, xml_doc, image): image_node = xml_doc.createElement('image') image_node.setAttribute('id', str(image['id'])) image_node.setAttribute('name', image['name']) - self._add_atom_links(xml_doc, image_node, image['links']) + self._create_link_nodes(xml_doc, image_node, image['links']) return image_node def _image_to_xml_detailed(self, xml_doc, image): @@ -297,7 +291,7 @@ class ImageXMLSerializer(wsgi.DictSerializer): metadata_node = self._create_metadata_node(xml_doc, metadata) image_node.appendChild(metadata_node) - self._add_atom_links(xml_doc, image_node, image['links']) + self._create_link_nodes(xml_doc, image_node, image['links']) return image_node @@ -310,25 +304,19 @@ class ImageXMLSerializer(wsgi.DictSerializer): if 'progress' in image: node.setAttribute('progress', str(image['progress'])) - def _create_server_node(self, xml_doc, server): - server_node = xml_doc.createElement('server') - server_node.setAttribute('id', str(server['id'])) - server_node.setAttribute('name', server['name']) - self._add_atom_links(xml_doc, server_node, server['links']) - return server_node - def _create_metadata_node(self, xml_doc, metadata): return self.metadata_serializer.meta_list_to_xml(xml_doc, metadata) - def _add_atom_links(self, xml_doc, node, links): - for link in links: - link_node = xml_doc.createElement('atom:link') - link_node.setAttribute('rel', link['rel']) - link_node.setAttribute('href', link['href']) - node.appendChild(link_node) + def _create_server_node(self, xml_doc, server): + return self.server_serializer.server_to_xml(xml_doc, server) - def _image_list_to_xml(self, xml_doc, images, image_to_xml): + 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 = image_to_xml(xml_doc, image) container_node.appendChild(item_node) @@ -342,34 +330,34 @@ class ImageXMLSerializer(wsgi.DictSerializer): image_to_xml = self._image_to_xml item_node = image_to_xml(xml_doc, image) self._add_xmlns(item_node) - return item_node.toprettyxml(indent=' ') - - def _image_list_to_xml_string(self, images, detailed): - xml_doc = minidom.Document() - if detailed: - image_to_xml = self._image_to_xml_detailed - else: - image_to_xml = self._image_to_xml - container_node = self._image_list_to_xml(xml_doc, images, image_to_xml) - - self._add_xmlns(container_node) - return container_node.toprettyxml(indent=' ') + self._add_atom_xmlns(item_node) + return item_node.toprettyxml(indent=' ', encoding='UTF-8') def index(self, images_dict): - return self._image_list_to_xml_string(images_dict['images'], - detailed=False) + xml_doc = minidom.Document() + node = self._image_list_to_xml(xml_doc, + images_dict['images'], + detailed=False) + return self.xml_string(node) def detail(self, images_dict): - return self._image_list_to_xml_string(images_dict['images'], - detailed=True) + xml_doc = minidom.Document() + node = self._image_list_to_xml(xml_doc, + images_dict['images'], + detailed=True) + return self.xml_string(node) def show(self, image_dict): - return self._image_to_xml_string(image_dict['image'], - detailed=True) + xml_doc = minidom.Document() + node = self._image_to_xml_detailed(xml_doc, + image_dict['image']) + return self.xml_string(node) def create(self, image_dict): - return self._image_to_xml_string(image_dict['image'], - detailed=True) + xml_doc = minidom.Document() + node = self._image_to_xml_detailed(xml_doc, + image_dict['image']) + return self.xml_string(node) def create_resource(version='1.0'): diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index fc1ab8d46..cff4e9b82 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -627,3 +627,15 @@ def create_resource(version='1.0'): return wsgi.Resource(controller, serializers=serializers, deserializers=deserializers) + + +class ServerXMLSerializer(wsgi.XMLDictSerializer): + def __init__(self, xmlns=wsgi.XMLNS_V11): + super(ServerXMLSerializer, self).__init__(xmlns=xmlns) + + def server_to_xml(self, xml_doc, server): + server_node = xml_doc.createElement('server') + server_node.setAttribute('id', str(server['id'])) + server_node.setAttribute('name', server['name']) + self._create_link_nodes(xml_doc, server_node, server['links']) + return server_node diff --git a/nova/api/openstack/wsgi.py b/nova/api/openstack/wsgi.py index 5b6e3cb1d..693c85b97 100644 --- a/nova/api/openstack/wsgi.py +++ b/nova/api/openstack/wsgi.py @@ -236,9 +236,14 @@ class XMLDictSerializer(DictSerializer): return node.toprettyxml(indent=' ', encoding='utf-8') + def xml_string(self, node): + self._add_xmlns(node) + return node.toprettyxml(indent=' ', encoding='UTF-8') + def _add_xmlns(self, node): if self.xmlns is not None: node.setAttribute('xmlns', self.xmlns) + 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,6 +299,13 @@ class XMLDictSerializer(DictSerializer): result.appendChild(node) return result + def _create_link_nodes(self, xml_doc, node, links): + for link in links: + link_node = xml_doc.createElement('atom:link') + link_node.setAttribute('rel', link['rel']) + link_node.setAttribute('href', link['href']) + node.appendChild(link_node) + class ResponseSerializer(object): """Encode the necessary pieces into a response object""" -- cgit From 14ee32f07536f5686794d2dbe8f1fad159af4dfe Mon Sep 17 00:00:00 2001 From: Alex Meade Date: Thu, 7 Jul 2011 14:09:23 -0400 Subject: Dried up images XML serialization --- nova/api/openstack/images.py | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/nova/api/openstack/images.py b/nova/api/openstack/images.py index bc7517c40..c540037fa 100644 --- a/nova/api/openstack/images.py +++ b/nova/api/openstack/images.py @@ -322,17 +322,6 @@ class ImageXMLSerializer(wsgi.XMLDictSerializer): container_node.appendChild(item_node) return container_node - def _image_to_xml_string(self, image, detailed): - xml_doc = minidom.Document() - if detailed: - image_to_xml = self._image_to_xml_detailed - else: - image_to_xml = self._image_to_xml - item_node = image_to_xml(xml_doc, image) - self._add_xmlns(item_node) - self._add_atom_xmlns(item_node) - return item_node.toprettyxml(indent=' ', encoding='UTF-8') - def index(self, images_dict): xml_doc = minidom.Document() node = self._image_list_to_xml(xml_doc, -- cgit From 162b9651c3e251d8acae764f08372f764597f8ca Mon Sep 17 00:00:00 2001 From: Alex Meade Date: Thu, 7 Jul 2011 14:27:27 -0400 Subject: Added param to keep current things from breaking until we update all of the xml serializers and view builders to reflect the current spec --- nova/api/openstack/images.py | 8 ++++---- nova/api/openstack/wsgi.py | 15 ++++++++------- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/nova/api/openstack/images.py b/nova/api/openstack/images.py index c540037fa..8f75fae4c 100644 --- a/nova/api/openstack/images.py +++ b/nova/api/openstack/images.py @@ -327,26 +327,26 @@ class ImageXMLSerializer(wsgi.XMLDictSerializer): node = self._image_list_to_xml(xml_doc, images_dict['images'], detailed=False) - return self.xml_string(node) + return self.xml_string(node, True) def detail(self, images_dict): xml_doc = minidom.Document() node = self._image_list_to_xml(xml_doc, images_dict['images'], detailed=True) - return self.xml_string(node) + return self.xml_string(node, True) def show(self, image_dict): xml_doc = minidom.Document() node = self._image_to_xml_detailed(xml_doc, image_dict['image']) - return self.xml_string(node) + return self.xml_string(node, True) def create(self, image_dict): xml_doc = minidom.Document() node = self._image_to_xml_detailed(xml_doc, image_dict['image']) - return self.xml_string(node) + return self.xml_string(node, True) def create_resource(version='1.0'): diff --git a/nova/api/openstack/wsgi.py b/nova/api/openstack/wsgi.py index 693c85b97..4ac7b56c9 100644 --- a/nova/api/openstack/wsgi.py +++ b/nova/api/openstack/wsgi.py @@ -232,18 +232,19 @@ 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.xml_string(node) - return node.toprettyxml(indent=' ', encoding='utf-8') - - def xml_string(self, node): - self._add_xmlns(node) + def 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 + def _add_xmlns(self, node, has_atom=False): if self.xmlns is not None: node.setAttribute('xmlns', self.xmlns) - node.setAttribute('xmlns:atom', "http://www.w3.org/2005/Atom") + 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.""" -- cgit From e9d712c52830725b24f20da7ff8662e97d22014d Mon Sep 17 00:00:00 2001 From: Alex Meade Date: Thu, 7 Jul 2011 14:51:12 -0400 Subject: Removed bookmark link from non detailed image viewbuilder --- nova/api/openstack/views/images.py | 13 ++++++------- nova/tests/api/openstack/test_images.py | 4 ---- 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/nova/api/openstack/views/images.py b/nova/api/openstack/views/images.py index 005341c62..fc4c1b55e 100644 --- a/nova/api/openstack/views/images.py +++ b/nova/api/openstack/views/images.py @@ -108,18 +108,17 @@ class ViewBuilderV11(ViewBuilder): href = self.generate_href(image_obj["id"]) bookmark = self.generate_bookmark(image_obj["id"]) - if detail: - image["metadata"] = image_obj.get("properties", {}) - image["links"] = [{ "rel": "self", "href": href, - }, - { - "rel": "bookmark", - "href": bookmark, }] + if detail: + image["metadata"] = image_obj.get("properties", {}) + image["links"].append({"rel": "bookmark", + "href": bookmark, + }) + return image def generate_bookmark(self, image_id): diff --git a/nova/tests/api/openstack/test_images.py b/nova/tests/api/openstack/test_images.py index cf68f5f53..d45195286 100644 --- a/nova/tests/api/openstack/test_images.py +++ b/nova/tests/api/openstack/test_images.py @@ -559,10 +559,6 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): "links": [{ "rel": "self", "href": href, - }, - { - "rel": "bookmark", - "href": bookmark, }], } self.assertTrue(test_image in response_list) -- cgit From 784c0a76967310843269d7f8be9df6bc292ff3c8 Mon Sep 17 00:00:00 2001 From: Devendra Modium Date: Thu, 7 Jul 2011 15:55:34 -0400 Subject: Comments for bugfix800759 and pep8 --- nova/scheduler/zone_manager.py | 12 +++--- nova/tests/test_zones.py | 88 ++++++++++++++++++++++++++++++++++-------- 2 files changed, 76 insertions(+), 24 deletions(-) diff --git a/nova/scheduler/zone_manager.py b/nova/scheduler/zone_manager.py index 2a0816118..8f928ce9e 100644 --- a/nova/scheduler/zone_manager.py +++ b/nova/scheduler/zone_manager.py @@ -125,7 +125,7 @@ class ZoneManager(object): # But it's likely to change once we understand what the Best-Match # code will need better. combined = {} # { _ : (min, max), ... } - stale_host_services = {} # { host1 : [svc1, svc2], host2 :[svc1]} + 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(): @@ -136,11 +136,10 @@ class ZoneManager(object): stale_host_services[host].append(service_name) continue for cap, value in service_dict.iteritems(): - if cap == "timestamp": # Timestamp is not needed + 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, 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) @@ -185,7 +184,7 @@ 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 + capabilities["timestamp"] = utils.utcnow() # Reported time service_caps[service_name] = capabilities self.service_states[host] = service_caps @@ -204,6 +203,5 @@ class ZoneManager(object): service_caps = self.service_states[host] for service in services: del service_caps[service] - if len(service_caps) == 0: # Delete host if no services + if len(service_caps) == 0: # Delete host if no services del self.service_states[host] - diff --git a/nova/tests/test_zones.py b/nova/tests/test_zones.py index f4661a328..a943fee27 100644 --- a/nova/tests/test_zones.py +++ b/nova/tests/test_zones.py @@ -199,31 +199,42 @@ class ZoneManagerTestCase(test.TestCase): self.assertFalse(zone_state.is_active) self.assertEquals(zone_state.name, None) - def test_host_service_caps_stale(self): + def test_host_service_caps_stale_no_stale_service(self): zm = zone_manager.ZoneManager() - expiry_time = (FLAGS.periodic_interval * 3) + 1 - # services just updated capabilities + # 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 - utils.clear_time_override() + 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(self): + + def test_delete_expired_host_services_del_one_service(self): zm = zone_manager.ZoneManager() # Delete one service in a host @@ -234,12 +245,19 @@ class ZoneManagerTestCase(test.TestCase): 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)) @@ -247,45 +265,70 @@ class ZoneManagerTestCase(test.TestCase): 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(self): + + def test_get_zone_capabilities_one_host(self): zm = zone_manager.ZoneManager() - expiry_time = (FLAGS.periodic_interval * 3) + 1 # 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 - utils.clear_time_override() 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))) - # Multiple services per host + 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))) - # Two host services among four become stale + 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) @@ -296,9 +339,15 @@ class ZoneManagerTestCase(test.TestCase): self.assertEquals(caps, dict(svc1_a=(1, 1), svc1_b=(2, 2), svc2_a=(7, 7), svc2_b=(8, 8))) - # Three host services among four become stale - zm.update_service_capabilities("svc2", "host1", dict(a=5, b=6)) + 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) @@ -311,11 +360,16 @@ class ZoneManagerTestCase(test.TestCase): caps = zm.get_zone_capabilities(None) self.assertEquals(caps, dict(svc1_a=(1, 1), svc1_b=(2, 2))) - # All the host services become stale - zm.update_service_capabilities("svc2", "host1", dict(a=5, b=6)) + 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, {}) - -- cgit From 1940ccffb6c22c39d6c21be4e84f20e350599f71 Mon Sep 17 00:00:00 2001 From: Alex Meade Date: Thu, 7 Jul 2011 16:19:21 -0400 Subject: Removed serverRef from some tests and viewbuilder --- nova/api/openstack/views/images.py | 4 +++- nova/tests/api/openstack/test_images.py | 33 ++------------------------------- 2 files changed, 5 insertions(+), 32 deletions(-) diff --git a/nova/api/openstack/views/images.py b/nova/api/openstack/views/images.py index fc4c1b55e..ae8e73678 100644 --- a/nova/api/openstack/views/images.py +++ b/nova/api/openstack/views/images.py @@ -98,7 +98,9 @@ class ViewBuilderV11(ViewBuilder): def _build_server(self, image, image_obj): try: - image['serverRef'] = image_obj['properties']['instance_ref'] + serverRef = image_obj['properties']['instance_ref'] + #TODO (ameade) get the server information + #image['server'] = ? except KeyError: return diff --git a/nova/tests/api/openstack/test_images.py b/nova/tests/api/openstack/test_images.py index d45195286..7e2240830 100644 --- a/nova/tests/api/openstack/test_images.py +++ b/nova/tests/api/openstack/test_images.py @@ -401,12 +401,13 @@ 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", @@ -648,7 +649,6 @@ 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', @@ -668,7 +668,6 @@ 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', @@ -689,7 +688,6 @@ 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', @@ -709,7 +707,6 @@ 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', @@ -1029,30 +1026,6 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): response = req.get_response(fakes.wsgi_app()) self.assertEqual(200, response.status_int) - def test_create_image_v1_1_actual_server_ref(self): - - serverRef = 'http://localhost/v1.1/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) - self.assertEqual(result['image']['serverRef'], serverRef) - - def test_create_image_v1_1_server_ref_bad_hostname(self): - - serverRef = 'http://asdf/v1.1/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_no_server_ref(self): body = dict(image=dict(name='Snapshot 1')) @@ -1441,8 +1414,6 @@ 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(""" Date: Thu, 7 Jul 2011 16:29:17 -0400 Subject: Temporarily moved create server node functionality into images.py Temporarily changed image XML tests to expect server entities with only ids --- nova/api/openstack/images.py | 7 +++++-- nova/api/openstack/servers.py | 12 ------------ nova/tests/api/openstack/test_images.py | 25 +++++-------------------- 3 files changed, 10 insertions(+), 34 deletions(-) diff --git a/nova/api/openstack/images.py b/nova/api/openstack/images.py index 8f75fae4c..a6af6da6f 100644 --- a/nova/api/openstack/images.py +++ b/nova/api/openstack/images.py @@ -269,7 +269,6 @@ class ImageXMLSerializer(wsgi.XMLDictSerializer): def __init__(self): self.metadata_serializer = image_metadata.ImageMetadataXMLSerializer() - self.server_serializer = servers.ServerXMLSerializer() def _image_to_xml(self, xml_doc, image): image_node = xml_doc.createElement('image') @@ -308,7 +307,11 @@ class ImageXMLSerializer(wsgi.XMLDictSerializer): return self.metadata_serializer.meta_list_to_xml(xml_doc, metadata) def _create_server_node(self, xml_doc, server): - return self.server_serializer.server_to_xml(xml_doc, server) + server_node = xml_doc.createElement('server') + server_node.setAttribute('id', str(server['id'])) + #server_node.setAttribute('name', server['name']) + #self._create_link_nodes(xml_doc, server_node, server['links']) + return server_node def _image_list_to_xml(self, xml_doc, images, detailed): container_node = xml_doc.createElement('images') diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index cff4e9b82..fc1ab8d46 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -627,15 +627,3 @@ def create_resource(version='1.0'): return wsgi.Resource(controller, serializers=serializers, deserializers=deserializers) - - -class ServerXMLSerializer(wsgi.XMLDictSerializer): - def __init__(self, xmlns=wsgi.XMLNS_V11): - super(ServerXMLSerializer, self).__init__(xmlns=xmlns) - - def server_to_xml(self, xml_doc, server): - server_node = xml_doc.createElement('server') - server_node.setAttribute('id', str(server['id'])) - server_node.setAttribute('name', server['name']) - self._create_link_nodes(xml_doc, server_node, server['links']) - return server_node diff --git a/nova/tests/api/openstack/test_images.py b/nova/tests/api/openstack/test_images.py index 7e2240830..d407057a8 100644 --- a/nova/tests/api/openstack/test_images.py +++ b/nova/tests/api/openstack/test_images.py @@ -1148,10 +1148,7 @@ class ImageXMLSerializationTest(test.TestCase): created="%(expected_now)s" status="ACTIVE" progress="80"> - - - - + value1 @@ -1219,10 +1216,7 @@ class ImageXMLSerializationTest(test.TestCase): updated="%(expected_now)s" created="%(expected_now)s" status="ACTIVE"> - - - - + @@ -1284,10 +1278,7 @@ class ImageXMLSerializationTest(test.TestCase): updated="%(expected_now)s" created="%(expected_now)s" status="ACTIVE"> - - - - + @@ -1503,10 +1494,7 @@ class ImageXMLSerializationTest(test.TestCase): updated="%(expected_now)s" created="%(expected_now)s" status="ACTIVE"> - - - - + @@ -1589,10 +1577,7 @@ class ImageXMLSerializationTest(test.TestCase): created="%(expected_now)s" status="SAVING" progress="80"> - - - - + value1 -- cgit From 7c90561e7288967c364a874cde13d88a8cb8fab4 Mon Sep 17 00:00:00 2001 From: Alex Meade Date: Thu, 7 Jul 2011 16:32:17 -0400 Subject: Changed name of xml_string to to_xml_string --- nova/api/openstack/images.py | 8 ++++---- nova/api/openstack/wsgi.py | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/nova/api/openstack/images.py b/nova/api/openstack/images.py index a6af6da6f..e5496ed94 100644 --- a/nova/api/openstack/images.py +++ b/nova/api/openstack/images.py @@ -330,26 +330,26 @@ class ImageXMLSerializer(wsgi.XMLDictSerializer): node = self._image_list_to_xml(xml_doc, images_dict['images'], detailed=False) - return self.xml_string(node, True) + return self.to_xml_string(node, True) def detail(self, images_dict): xml_doc = minidom.Document() node = self._image_list_to_xml(xml_doc, images_dict['images'], detailed=True) - return self.xml_string(node, True) + return self.to_xml_string(node, True) def show(self, image_dict): xml_doc = minidom.Document() node = self._image_to_xml_detailed(xml_doc, image_dict['image']) - return self.xml_string(node, True) + return self.to_xml_string(node, True) def create(self, image_dict): xml_doc = minidom.Document() node = self._image_to_xml_detailed(xml_doc, image_dict['image']) - return self.xml_string(node, True) + return self.to_xml_string(node, True) def create_resource(version='1.0'): diff --git a/nova/api/openstack/wsgi.py b/nova/api/openstack/wsgi.py index 4ac7b56c9..f13bd6cc1 100644 --- a/nova/api/openstack/wsgi.py +++ b/nova/api/openstack/wsgi.py @@ -232,9 +232,9 @@ class XMLDictSerializer(DictSerializer): doc = minidom.Document() node = self._to_xml_node(doc, self.metadata, root_key, data[root_key]) - return self.xml_string(node) + return self.to_xml_string(node) - def xml_string(self, node, has_atom=False): + def to_xml_string(self, node, has_atom=False): self._add_xmlns(node, has_atom) return node.toprettyxml(indent=' ', encoding='UTF-8') -- cgit From d4540d2abde8db9519d8e4dad9b57a116a2c8b9e Mon Sep 17 00:00:00 2001 From: Alex Meade Date: Thu, 7 Jul 2011 16:41:22 -0400 Subject: Updated _create_link_nodes to be consistent with other create_*_nodes --- nova/api/openstack/images.py | 15 ++++++++++++--- nova/api/openstack/wsgi.py | 6 ++++-- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/nova/api/openstack/images.py b/nova/api/openstack/images.py index e5496ed94..bf98a5cdd 100644 --- a/nova/api/openstack/images.py +++ b/nova/api/openstack/images.py @@ -274,7 +274,10 @@ class ImageXMLSerializer(wsgi.XMLDictSerializer): image_node = xml_doc.createElement('image') image_node.setAttribute('id', str(image['id'])) image_node.setAttribute('name', image['name']) - self._create_link_nodes(xml_doc, image_node, image['links']) + 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): @@ -290,7 +293,10 @@ class ImageXMLSerializer(wsgi.XMLDictSerializer): metadata_node = self._create_metadata_node(xml_doc, metadata) image_node.appendChild(metadata_node) - self._create_link_nodes(xml_doc, image_node, image['links']) + link_nodes = self._create_link_nodes(xml_doc, + image['links']) + for link_node in link_nodes: + image_node.appendChild(link_node) return image_node @@ -310,7 +316,10 @@ class ImageXMLSerializer(wsgi.XMLDictSerializer): server_node = xml_doc.createElement('server') server_node.setAttribute('id', str(server['id'])) #server_node.setAttribute('name', server['name']) - #self._create_link_nodes(xml_doc, server_node, server['links']) + #link_nodes = self._create_link_nodes(xml_doc, + # image['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): diff --git a/nova/api/openstack/wsgi.py b/nova/api/openstack/wsgi.py index f13bd6cc1..03f83001a 100644 --- a/nova/api/openstack/wsgi.py +++ b/nova/api/openstack/wsgi.py @@ -300,12 +300,14 @@ class XMLDictSerializer(DictSerializer): result.appendChild(node) return result - def _create_link_nodes(self, xml_doc, node, links): + 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']) - node.appendChild(link_node) + link_nodes.append(link_node) + return link_nodes class ResponseSerializer(object): -- cgit From 49697c5fa1ce3d4b1ae366baeda3336f5edab5a8 Mon Sep 17 00:00:00 2001 From: Devendra Modium Date: Thu, 7 Jul 2011 16:47:23 -0400 Subject: Merging issues --- Authors | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Authors b/Authors index 67cd38bcf..052f2d964 100644 --- a/Authors +++ b/Authors @@ -20,6 +20,7 @@ Dan Prince Dave Walker David Pravec Dean Troyer +Devendra Modium Devin Carlen Ed Leafe Eldar Nugaev @@ -93,4 +94,3 @@ Yoshiaki Tamura Youcef Laribi Yuriy Taraday Zhixue Wu -Devendra Modium -- cgit From a8c9082c701a65f221f218cd8baa92b3859fc0ab Mon Sep 17 00:00:00 2001 From: Alex Meade Date: Thu, 7 Jul 2011 16:52:32 -0400 Subject: Added server entity to images that only has id --- nova/api/openstack/views/images.py | 5 +++-- nova/tests/api/openstack/test_images.py | 15 +++++++++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/nova/api/openstack/views/images.py b/nova/api/openstack/views/images.py index ae8e73678..d1f89a785 100644 --- a/nova/api/openstack/views/images.py +++ b/nova/api/openstack/views/images.py @@ -99,8 +99,9 @@ class ViewBuilderV11(ViewBuilder): def _build_server(self, image, image_obj): try: serverRef = image_obj['properties']['instance_ref'] - #TODO (ameade) get the server information - #image['server'] = ? + image['server'] = { + "id": common.get_id_from_href(serverRef), + } except KeyError: return diff --git a/nova/tests/api/openstack/test_images.py b/nova/tests/api/openstack/test_images.py index d407057a8..87e5c6d7f 100644 --- a/nova/tests/api/openstack/test_images.py +++ b/nova/tests/api/openstack/test_images.py @@ -411,6 +411,9 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): "updated": self.NOW_API_FORMAT, "created": self.NOW_API_FORMAT, "status": "QUEUED", + 'server': { + 'id': 42, + }, "metadata": { "instance_ref": "http://localhost/v1.1/servers/42", "user_id": "1", @@ -652,6 +655,9 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): 'updated': self.NOW_API_FORMAT, 'created': self.NOW_API_FORMAT, 'status': 'QUEUED', + 'server': { + 'id': 42, + }, "links": [{ "rel": "self", "href": "http://localhost/v1.1/images/124", @@ -672,6 +678,9 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): 'created': self.NOW_API_FORMAT, 'status': 'SAVING', 'progress': 0, + 'server': { + 'id': 42, + }, "links": [{ "rel": "self", "href": "http://localhost/v1.1/images/125", @@ -691,6 +700,9 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): 'updated': self.NOW_API_FORMAT, 'created': self.NOW_API_FORMAT, 'status': 'ACTIVE', + 'server': { + 'id': 42, + }, "links": [{ "rel": "self", "href": "http://localhost/v1.1/images/126", @@ -710,6 +722,9 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): 'updated': self.NOW_API_FORMAT, 'created': self.NOW_API_FORMAT, 'status': 'FAILED', + 'server': { + 'id': 42, + }, "links": [{ "rel": "self", "href": "http://localhost/v1.1/images/127", -- cgit From 9f3a309eab3af46aa3f8ac2767b7edcdeb49e043 Mon Sep 17 00:00:00 2001 From: "Kevin L. Mitchell" Date: Thu, 7 Jul 2011 16:46:55 -0500 Subject: unit test suite for the multinic extension --- .../api/openstack/contrib/test_multinic_xs.py | 88 ++++++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 nova/tests/api/openstack/contrib/test_multinic_xs.py 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..e531435a0 --- /dev/null +++ b/nova/tests/api/openstack/contrib/test_multinic_xs.py @@ -0,0 +1,88 @@ +# Copyright 2011 Eldar Nugaev +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import json +import 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) + + +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) + + +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.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.assertNotEqual(last_add_fixed_ip, (None, None)) + + 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): + 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, 501) -- cgit From 8c8e92b2662a3ab9aa2fd71bef48d95408ebb89b Mon Sep 17 00:00:00 2001 From: Isaku Yamahata Date: Fri, 8 Jul 2011 12:09:50 +0900 Subject: sqlalchemy/migrate: resolved version conflict --- .../versions/028_add_root_device_name.py | 47 ---------------------- .../versions/032_add_root_device_name.py | 47 ++++++++++++++++++++++ 2 files changed, 47 insertions(+), 47 deletions(-) delete mode 100644 nova/db/sqlalchemy/migrate_repo/versions/028_add_root_device_name.py create mode 100644 nova/db/sqlalchemy/migrate_repo/versions/032_add_root_device_name.py diff --git a/nova/db/sqlalchemy/migrate_repo/versions/028_add_root_device_name.py b/nova/db/sqlalchemy/migrate_repo/versions/028_add_root_device_name.py deleted file mode 100644 index 6b98b9890..000000000 --- a/nova/db/sqlalchemy/migrate_repo/versions/028_add_root_device_name.py +++ /dev/null @@ -1,47 +0,0 @@ -# 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/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') -- cgit From 2c3eeb5f9f5f78d9cb8fb3e37d5b5e1610d32499 Mon Sep 17 00:00:00 2001 From: John Tran Date: Thu, 7 Jul 2011 20:43:50 -0700 Subject: ec2 api _get_image method logic flaw that strips the hex16 digit off of the image name, and does a search against the db for it and ignores that it may not be the correct image, such as if doing a search for aki-0000009, yet that image name doesn't exist, it strips off aki- and looks for any image_id 0000009 and if there was an image match that happens to be an ami instead of aki, it will go ahead and deregister that. That behavior is unintended, so added logic to ensure that the original request image_id matches the type of image being returned from database by matching against container_format attr --- nova/api/ec2/cloud.py | 6 +++++- nova/tests/test_cloud.py | 30 +++++++++++++++++++++++------- 2 files changed, 28 insertions(+), 8 deletions(-) diff --git a/nova/api/ec2/cloud.py b/nova/api/ec2/cloud.py index 9be30cf75..a4f4dae4f 100644 --- a/nova/api/ec2/cloud.py +++ b/nova/api/ec2/cloud.py @@ -1088,12 +1088,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 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.""" diff --git a/nova/tests/test_cloud.py b/nova/tests/test_cloud.py index bf7a2b7ca..d71a03aff 100644 --- a/nova/tests/test_cloud.py +++ b/nova/tests/test_cloud.py @@ -67,7 +67,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) @@ -418,7 +419,8 @@ class CloudTestCase(test.TestCase): 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): @@ -448,7 +450,8 @@ class CloudTestCase(test.TestCase): 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) @@ -460,7 +463,8 @@ class CloudTestCase(test.TestCase): 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 +498,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 +623,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 +637,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 +653,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) -- cgit From 4f973269e84adb10ac3959ef255ecc60cc90620e Mon Sep 17 00:00:00 2001 From: Isaku Yamahata Date: Fri, 8 Jul 2011 15:36:18 +0900 Subject: nova/compute/api.py: fixed mismerge --- nova/compute/api.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/nova/compute/api.py b/nova/compute/api.py index 5350b8f28..8530deb2d 100644 --- a/nova/compute/api.py +++ b/nova/compute/api.py @@ -248,7 +248,7 @@ class API(base.Base): 'vm_mode': vm_mode, 'root_device_name': root_device_name} - return (num_instances, base_options, security_groups, image) + return (num_instances, base_options, image) def _update_image_block_device_mapping(self, elevated_context, instance_id, mappings): @@ -300,7 +300,7 @@ class API(base.Base): values) def create_db_entry_for_new_instance(self, context, image, base_options, - security_groups, block_device_mapping, num=1): + security_group, block_device_mapping, num=1): """Create an entry in the DB for this new instance, including any related table updates (such as security group, etc). @@ -401,8 +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, security_groups, image = \ - 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, @@ -440,8 +439,7 @@ class API(base.Base): Returns a list of instance dicts. """ - num_instances, base_options, security_groups, image = \ - 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, @@ -451,11 +449,12 @@ 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, image, - base_options, security_groups, + base_options, security_group, block_device_mapping, num=num) instances.append(instance) instance_id = instance['id'] -- cgit From 58d7fa8bf8610ac2fa65e974061bf8ae78ca321f Mon Sep 17 00:00:00 2001 From: Isaku Yamahata Date: Fri, 8 Jul 2011 15:36:40 +0900 Subject: tests/test_cloud: make an unit test, test_create_image, happy --- nova/tests/test_cloud.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/nova/tests/test_cloud.py b/nova/tests/test_cloud.py index bd308f865..77b0f0a2e 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() -- cgit From d5307a2e1575778fcbfcf3d8ad65733be7544a54 Mon Sep 17 00:00:00 2001 From: Isaku Yamahata Date: Fri, 8 Jul 2011 18:35:01 +0900 Subject: image/fake: added teardown method Unit tests may alter images in FakeImageService which has pre-defined images. Since some unit tests depend on those images, so it needs to be cleaned up after image alternation. Otherwise running many unit tests may fail. --- nova/image/fake.py | 11 ++++++++++- nova/test.py | 4 ++++ 2 files changed, 14 insertions(+), 1 deletion(-) 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/test.py b/nova/test.py index b2599d4be..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() -- cgit From 921fee22ff42852b1ee0d7f3d051b44d60afd975 Mon Sep 17 00:00:00 2001 From: "Kevin L. Mitchell" Date: Fri, 8 Jul 2011 13:06:37 -0500 Subject: Add support for remove_fixed_ip() --- nova/api/openstack/contrib/multinic.py | 20 +++++++++++-- .../api/openstack/contrib/test_multinic_xs.py | 35 ++++++++++++++++++++-- 2 files changed, 50 insertions(+), 5 deletions(-) diff --git a/nova/api/openstack/contrib/multinic.py b/nova/api/openstack/contrib/multinic.py index 164af79b0..e202bd51c 100644 --- a/nova/api/openstack/contrib/multinic.py +++ b/nova/api/openstack/contrib/multinic.py @@ -26,6 +26,8 @@ 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): def __init__(self, *args, **kwargs): super(Multinic, self).__init__(*args, **kwargs) @@ -79,5 +81,19 @@ class Multinic(extensions.ExtensionDescriptor): return exc.HTTPAccepted() def _remove_fixed_ip(self, input_dict, req, id): - # Not yet implemented - raise faults.Fault(exc.HTTPNotImplemented()) + """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/tests/api/openstack/contrib/test_multinic_xs.py b/nova/tests/api/openstack/contrib/test_multinic_xs.py index e531435a0..b9f4456bb 100644 --- a/nova/tests/api/openstack/contrib/test_multinic_xs.py +++ b/nova/tests/api/openstack/contrib/test_multinic_xs.py @@ -1,4 +1,4 @@ -# Copyright 2011 Eldar Nugaev +# Copyright 2011 OpenStack LLC. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may @@ -24,6 +24,7 @@ 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): @@ -32,6 +33,12 @@ def compute_api_add_fixed_ip(self, context, instance_id, network_id): 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() @@ -43,6 +50,9 @@ class FixedIpTest(test.TestCase): fakes.stub_out_auth(self.stubs) self.stubs.Set(compute.api.API, "add_fixed_ip", compute_api_add_fixed_ip) + # TODO(Vek): Fails until remove_fixed_ip() added + # self.stubs.Set(compute.api.API, "remove_fixed_ip", + # compute_api_remove_fixed_ip) self.context = context.get_admin_context() def tearDown(self): @@ -61,7 +71,7 @@ class FixedIpTest(test.TestCase): resp = req.get_response(fakes.wsgi_app()) self.assertEqual(resp.status_int, 202) - self.assertNotEqual(last_add_fixed_ip, (None, None)) + self.assertEqual(last_add_fixed_ip, ('test_inst', 'test_net')) def test_add_fixed_ip_no_network(self): global last_add_fixed_ip @@ -78,6 +88,9 @@ class FixedIpTest(test.TestCase): 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' @@ -85,4 +98,20 @@ class FixedIpTest(test.TestCase): req.headers['content-type'] = 'application/json' resp = req.get_response(fakes.wsgi_app()) - self.assertEqual(resp.status_int, 501) + # TODO(Vek): Fails until remove_fixed_ip() added + 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)) -- cgit From c98f37c00d802abf2ac85cb6c800f39e1b067d72 Mon Sep 17 00:00:00 2001 From: "Kevin L. Mitchell" Date: Fri, 8 Jul 2011 14:10:32 -0500 Subject: Add docstrings for multinic extension --- nova/api/openstack/contrib/multinic.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/nova/api/openstack/contrib/multinic.py b/nova/api/openstack/contrib/multinic.py index e202bd51c..841061721 100644 --- a/nova/api/openstack/contrib/multinic.py +++ b/nova/api/openstack/contrib/multinic.py @@ -29,26 +29,50 @@ 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 @@ -65,6 +89,7 @@ class Multinic(extensions.ExtensionDescriptor): 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']: @@ -82,6 +107,7 @@ class Multinic(extensions.ExtensionDescriptor): 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']: -- cgit From 32a3cf25721173014fbd20c8f2954f015316f439 Mon Sep 17 00:00:00 2001 From: Brian Waldon Date: Fri, 8 Jul 2011 15:19:19 -0400 Subject: removing Content-Length requirement --- nova/api/openstack/wsgi.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/nova/api/openstack/wsgi.py b/nova/api/openstack/wsgi.py index 6726531be..1a33b7744 100644 --- a/nova/api/openstack/wsgi.py +++ b/nova/api/openstack/wsgi.py @@ -176,14 +176,8 @@ class RequestDeserializer(object): LOG.debug(_("No Content-Type provided in request")) return {} - if 'Content-Length' not in request.headers: - msg = _("No Content-Length provided in request") - LOG.debug(msg) - return {} - - if not request.content_length > 0: - msg = _("Request has Content-Length of zero") - LOG.debug(msg) + if not len(request.body) > 0: + LOG.debug(_("Empty body provided in request")) return {} deserializer = self.get_body_deserializer(content_type) -- cgit From 17fceb2368d63dd937dc5e9385158c01130557a7 Mon Sep 17 00:00:00 2001 From: John Tran Date: Fri, 8 Jul 2011 14:19:35 -0700 Subject: peer review fix - per vish: 'This method automatically converts unknown formats to ami, which is the same logic used to display unknown images in the ec2 api. This will allow you to properly deregister raw images, etc.' --- nova/api/ec2/cloud.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nova/api/ec2/cloud.py b/nova/api/ec2/cloud.py index a4f4dae4f..ee263f614 100644 --- a/nova/api/ec2/cloud.py +++ b/nova/api/ec2/cloud.py @@ -1095,7 +1095,7 @@ class CloudController(object): except exception.NotFound: raise exception.ImageNotFound(image_id=ec2_id) image_type = ec2_id.split('-')[0] - if image.get('container_format') != image_type: + if self._image_type(image.get('container_format')) != image_type: raise exception.ImageNotFound(image_id=ec2_id) return image -- cgit From fe8da67779dbb03654b1cce90eeafdb323507673 Mon Sep 17 00:00:00 2001 From: Brian Waldon Date: Fri, 8 Jul 2011 17:21:58 -0400 Subject: easing up content-type restrictions --- nova/api/openstack/wsgi.py | 2 +- nova/tests/api/openstack/test_wsgi.py | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/nova/api/openstack/wsgi.py b/nova/api/openstack/wsgi.py index 1a33b7744..bd91aed60 100644 --- a/nova/api/openstack/wsgi.py +++ b/nova/api/openstack/wsgi.py @@ -52,7 +52,7 @@ class Request(webob.Request): content_type = self.content_type if content_type not in allowed_types: - raise exception.InvalidContentType(content_type=content_type) + return None else: return content_type diff --git a/nova/tests/api/openstack/test_wsgi.py b/nova/tests/api/openstack/test_wsgi.py index 938637207..7ab39ed5a 100644 --- a/nova/tests/api/openstack/test_wsgi.py +++ b/nova/tests/api/openstack/test_wsgi.py @@ -18,8 +18,7 @@ class RequestTest(test.TestCase): request = wsgi.Request.blank('/tests/123', method='POST') request.headers["Content-Type"] = "text/html" request.body = "asdf
" - self.assertRaises(exception.InvalidContentType, - request.get_content_type) + self.assertEqual(request.get_content_type(), None) def test_content_type_with_charset(self): request = wsgi.Request.blank('/tests/123') -- cgit From b5ca0d793826ac10ee41be84f18d64b09113aa80 Mon Sep 17 00:00:00 2001 From: Brian Waldon Date: Fri, 8 Jul 2011 17:40:56 -0400 Subject: refactor --- nova/api/openstack/wsgi.py | 25 ++++++++++++++----------- nova/tests/api/openstack/test_wsgi.py | 3 ++- 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/nova/api/openstack/wsgi.py b/nova/api/openstack/wsgi.py index bd91aed60..7a8376722 100644 --- a/nova/api/openstack/wsgi.py +++ b/nova/api/openstack/wsgi.py @@ -52,9 +52,9 @@ class Request(webob.Request): content_type = self.content_type if content_type not in allowed_types: - return None - else: - return content_type + raise exception.InvalidContentType(content_type=content_type) + + return content_type class TextDeserializer(object): @@ -171,19 +171,22 @@ class RequestDeserializer(object): 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 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 {} + if not len(request.body) > 0: + LOG.debug(_("Empty body provided in request")) + return {} + try: deserializer = self.get_body_deserializer(content_type) - except exception.InvalidContentType: - LOG.debug(_("Unable to read body as provided Content-Type")) + LOG.debug(_("Unable to deserialize body as provided Content-Type")) raise return deserializer.deserialize(request.body, action) diff --git a/nova/tests/api/openstack/test_wsgi.py b/nova/tests/api/openstack/test_wsgi.py index 7ab39ed5a..938637207 100644 --- a/nova/tests/api/openstack/test_wsgi.py +++ b/nova/tests/api/openstack/test_wsgi.py @@ -18,7 +18,8 @@ class RequestTest(test.TestCase): request = wsgi.Request.blank('/tests/123', method='POST') request.headers["Content-Type"] = "text/html" request.body = "asdf
" - self.assertEqual(request.get_content_type(), None) + self.assertRaises(exception.InvalidContentType, + request.get_content_type) def test_content_type_with_charset(self): request = wsgi.Request.blank('/tests/123') -- cgit From d326033f5ed4e8e24efddd4acf080ebfeb0caf2c Mon Sep 17 00:00:00 2001 From: Devendra Modium Date: Mon, 11 Jul 2011 10:48:32 -0400 Subject: Missing Author updated --- Authors | 1 + 1 file changed, 1 insertion(+) diff --git a/Authors b/Authors index 052f2d964..8d117c489 100644 --- a/Authors +++ b/Authors @@ -44,6 +44,7 @@ John Dewey John Tran Jonathan Bryce Jordan Rinke +Joseph Suh Josh Durgin Josh Kearney Josh Kleinpeter -- cgit From e6ebcbd911467ed1a9a468a350c5488ac83811bd Mon Sep 17 00:00:00 2001 From: Sandy Walsh Date: Mon, 11 Jul 2011 07:49:34 -0700 Subject: pre trunk merge --- nova/exception.py | 4 ++++ nova/network/manager.py | 9 +++++---- nova/tests/test_network.py | 2 +- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/nova/exception.py b/nova/exception.py index a6776b64f..988940d6a 100644 --- a/nova/exception.py +++ b/nova/exception.py @@ -377,6 +377,10 @@ class FixedIpNotFoundForInstance(FixedIpNotFound): message = _("Instance %(instance_id)s has zero fixed ips.") +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/network/manager.py b/nova/network/manager.py index 0432179dd..f4078b022 100644 --- a/nova/network/manager.py +++ b/nova/network/manager.py @@ -482,12 +482,13 @@ class NetworkManager(manager.SchedulerDependentManager): def remove_fixed_ip_from_instance(self, context, instance_id, address): """Removes a fixed ip from an instance from specified network.""" - addresses = self.db.fixed_ip_get_by_instance(context, instance_id) - for addr in addresses: - if addr['address'] == address: + 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.FixedIpNotFound(id=address) + 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.""" diff --git a/nova/tests/test_network.py b/nova/tests/test_network.py index d5cab47cf..b09021e13 100644 --- a/nova/tests/test_network.py +++ b/nova/tests/test_network.py @@ -268,6 +268,6 @@ class CommonNetworkTestCase(test.TestCase): def test_remove_fixed_ip_from_instance_bad_input(self): manager = self.FakeNetworkManager() - self.assertRaises(exception.FixedIpNotFound, + self.assertRaises(exception.FixedIpNotFoundForSpecificInstance, manager.remove_fixed_ip_from_instance, None, 99, 'bad input') -- cgit From 63ac91a2f62c2f07c7458e4f55a8e10e182b9fdf Mon Sep 17 00:00:00 2001 From: Brian Waldon Date: Mon, 11 Jul 2011 10:50:49 -0400 Subject: adding 204 response code --- nova/api/openstack/servers.py | 33 +++++++++++++++++++++++--------- nova/tests/api/openstack/test_servers.py | 16 ++++++++++++++++ 2 files changed, 40 insertions(+), 9 deletions(-) diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index eacc2109f..241ba970c 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -104,15 +104,6 @@ class Controller(object): 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 @@ -420,6 +411,15 @@ 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'] @@ -482,6 +482,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'] @@ -597,6 +606,12 @@ class ControllerV11(Controller): return self.helper._get_server_admin_password_new_style(server) +class ServersHTTPSerializer(wsgi.DictSerializer): + + def delete(self, response): + response.status_int = 204 + + def create_resource(version='1.0'): controller = { '1.0': ControllerV10, diff --git a/nova/tests/api/openstack/test_servers.py b/nova/tests/api/openstack/test_servers.py index 1f369c4c8..bb4a0510a 100644 --- a/nova/tests/api/openstack/test_servers.py +++ b/nova/tests/api/openstack/test_servers.py @@ -1433,6 +1433,22 @@ class ServersTest(test.TestCase): self.assertEqual(res.status, '202 Accepted') self.assertEqual(self.server_delete_called, True) + 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))) -- cgit From d2c628c0ecbae048fabaf60eff2afd4b8a9cd918 Mon Sep 17 00:00:00 2001 From: "Kevin L. Mitchell" Date: Mon, 11 Jul 2011 10:18:17 -0500 Subject: Comment out these two asserts; Sandy will uncomment in his merge-prop --- nova/tests/api/openstack/contrib/test_multinic_xs.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nova/tests/api/openstack/contrib/test_multinic_xs.py b/nova/tests/api/openstack/contrib/test_multinic_xs.py index b9f4456bb..484cd1c17 100644 --- a/nova/tests/api/openstack/contrib/test_multinic_xs.py +++ b/nova/tests/api/openstack/contrib/test_multinic_xs.py @@ -99,8 +99,8 @@ class FixedIpTest(test.TestCase): resp = req.get_response(fakes.wsgi_app()) # TODO(Vek): Fails until remove_fixed_ip() added - self.assertEqual(resp.status_int, 202) - self.assertEqual(last_remove_fixed_ip, ('test_inst', '10.10.10.1')) + # 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 -- cgit From 334f2215f0533e8181d40cd086e927e7913739f2 Mon Sep 17 00:00:00 2001 From: Brian Waldon Date: Mon, 11 Jul 2011 12:31:38 -0400 Subject: minor refactoring --- nova/api/openstack/wsgi.py | 52 +++++++++++++++------- nova/tests/api/openstack/test_wsgi.py | 83 ++++++++++++++++++++++++----------- 2 files changed, 94 insertions(+), 41 deletions(-) diff --git a/nova/api/openstack/wsgi.py b/nova/api/openstack/wsgi.py index 7a8376722..9a273bf6e 100644 --- a/nova/api/openstack/wsgi.py +++ b/nova/api/openstack/wsgi.py @@ -57,16 +57,26 @@ class Request(webob.Request): return content_type -class TextDeserializer(object): - """Custom request body deserialization based on controller action name.""" +class ActionDispatcher(object): + """Maps method name to local methods through action name.""" - def deserialize(self, datastring, action='default'): - """Find local deserialization method and parse request body.""" + 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, data): + raise NotImplementedError() + + +class TextDeserializer(ActionDispatcher): + """Default request body deserialization""" + + def deserialize(self, datastring, action='default'): + return self.dispatch(datastring, action=action) def default(self, datastring): - """Default deserialization code should live here""" return {} @@ -128,8 +138,13 @@ class XMLDeserializer(TextDeserializer): return {'body': self._from_xml(datastring)} -class RequestHeadersDeserializer(object): +class RequestHeadersDeserializer(ActionDispatcher): + """Default request headers deserializer""" + def deserialize(self, request, action): + return self.dispatch(request, action=action) + + def default(self, request): return {} @@ -220,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) @@ -320,8 +333,13 @@ class XMLDictSerializer(DictSerializer): return result -class ResponseHeadersSerializer(object): +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 @@ -410,7 +428,9 @@ class Resource(wsgi.Application): #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) + response = self.serializer.serialize(action_result, + accept, + action=action) else: response = action_result diff --git a/nova/tests/api/openstack/test_wsgi.py b/nova/tests/api/openstack/test_wsgi.py index 938637207..9b68f76b0 100644 --- a/nova/tests/api/openstack/test_wsgi.py +++ b/nova/tests/api/openstack/test_wsgi.py @@ -75,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): @@ -116,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): @@ -189,6 +199,22 @@ class XMLDeserializerTest(test.TestCase): 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): @@ -199,12 +225,17 @@ class ResponseSerializerTest(test.TestCase): def serialize(self, data, action='default'): return 'pew_xml' + 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(self.body_serializers) + self.serializer = wsgi.ResponseSerializer(self.body_serializers, + HeadersSerializer()) def tearDown(self): pass @@ -223,6 +254,7 @@ class ResponseSerializerTest(test.TestCase): 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,6 +262,7 @@ class ResponseSerializerTest(test.TestCase): {}, 'application/unknown') + class RequestDeserializerTest(test.TestCase): def setUp(self): class JSONDeserializer(object): -- cgit From ac894f7d4dfde9c4d818007e105860661b00fd04 Mon Sep 17 00:00:00 2001 From: Brian Waldon Date: Mon, 11 Jul 2011 12:33:53 -0400 Subject: pep8 --- nova/tests/api/openstack/test_wsgi.py | 1 - 1 file changed, 1 deletion(-) diff --git a/nova/tests/api/openstack/test_wsgi.py b/nova/tests/api/openstack/test_wsgi.py index 9b68f76b0..5bdda7c7e 100644 --- a/nova/tests/api/openstack/test_wsgi.py +++ b/nova/tests/api/openstack/test_wsgi.py @@ -262,7 +262,6 @@ class ResponseSerializerTest(test.TestCase): {}, 'application/unknown') - class RequestDeserializerTest(test.TestCase): def setUp(self): class JSONDeserializer(object): -- cgit From 6a0b3b8a143e60334dab7ed541caca1eba27c88b Mon Sep 17 00:00:00 2001 From: Brian Waldon Date: Mon, 11 Jul 2011 12:34:07 -0400 Subject: adding headers serializer --- nova/api/openstack/servers.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index 4cda8981c..15d23dc09 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -606,7 +606,7 @@ class ControllerV11(Controller): return self.helper._get_server_admin_password_new_style(server) -class ServersHTTPSerializer(wsgi.DictSerializer): +class HeadersSerializer(wsgi.DictSerializer): def delete(self, response): response.status_int = 204 @@ -639,6 +639,8 @@ def create_resource(version='1.0'): '1.1': wsgi.XMLNS_V11, }[version] + headers_serializer = HeadersSerializer() + body_serializers = { 'application/xml': wsgi.XMLDictSerializer(metadata=metadata, xmlns=xmlns), @@ -648,7 +650,7 @@ def create_resource(version='1.0'): 'application/xml': helper.ServerXMLDeserializer(), } - serializer = wsgi.ResponseSerializer(body_serializers) + serializer = wsgi.ResponseSerializer(body_serializers, headers_serializer) deserializer = wsgi.RequestDeserializer(body_deserializers) return wsgi.Resource(controller, deserializer, serializer) -- cgit From ad23d0f354b8698b5314ed2a55e5a4d17abffba0 Mon Sep 17 00:00:00 2001 From: Brian Waldon Date: Mon, 11 Jul 2011 13:16:22 -0400 Subject: allowing controllers to return Nonew --- nova/api/openstack/wsgi.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nova/api/openstack/wsgi.py b/nova/api/openstack/wsgi.py index 9a273bf6e..8eff9e441 100644 --- a/nova/api/openstack/wsgi.py +++ b/nova/api/openstack/wsgi.py @@ -427,7 +427,7 @@ class Resource(wsgi.Application): 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: + if type(action_result) is dict or action_result is None: response = self.serializer.serialize(action_result, accept, action=action) -- cgit From 02b0ca3e44626623c70d04ccaa50af8c75d640af Mon Sep 17 00:00:00 2001 From: Brian Waldon Date: Mon, 11 Jul 2011 13:29:56 -0400 Subject: updating code to implement tests --- nova/api/openstack/servers.py | 4 ++-- nova/tests/integrated/api/client.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index 15d23dc09..12af44a8d 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -606,9 +606,9 @@ class ControllerV11(Controller): return self.helper._get_server_admin_password_new_style(server) -class HeadersSerializer(wsgi.DictSerializer): +class HeadersSerializer(wsgi.ResponseHeadersSerializer): - def delete(self, response): + def delete(self, response, data): response.status_int = 204 diff --git a/nova/tests/integrated/api/client.py b/nova/tests/integrated/api/client.py index 3d255b40c..59cc3b564 100644 --- a/nova/tests/integrated/api/client.py +++ b/nova/tests/integrated/api/client.py @@ -174,7 +174,7 @@ class TestOpenStackClient(object): 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): -- cgit From dcd1cbc5e2ba5ee2a54dda4558d84d4ffc9850b9 Mon Sep 17 00:00:00 2001 From: Naveed Massjouni Date: Mon, 11 Jul 2011 15:12:47 -0400 Subject: Making the xen plugins rpm to be noarch. --- .../xenserver/xenapi/contrib/rpmbuild/SPECS/openstack-xen-plugins.spec | 1 + 1 file changed, 1 insertion(+) 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 -- cgit From 47c7a43ffcac3518543fdcc94ab54577249d9ced Mon Sep 17 00:00:00 2001 From: "matt.dietz@rackspace.com" <> Date: Mon, 11 Jul 2011 15:08:53 -0500 Subject: Tests --- nova/tests/api/openstack/test_servers.py | 36 ++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/nova/tests/api/openstack/test_servers.py b/nova/tests/api/openstack/test_servers.py index b6b5a1e76..e5b9dd766 100644 --- a/nova/tests/api/openstack/test_servers.py +++ b/nova/tests/api/openstack/test_servers.py @@ -1433,6 +1433,42 @@ 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' + req.body = json.dumps(body) + + res = req.get_response(fakes.wsgi_app()) + + self.assertEqual(res.status_int, 400) + def test_resize_server(self): req = self.webreq('/1/action', 'POST', dict(resize=dict(flavorId=3))) -- cgit From e20e6a88c66aac240f18e6da604fc1e048f5b5ce Mon Sep 17 00:00:00 2001 From: "matt.dietz@rackspace.com" <> Date: Mon, 11 Jul 2011 15:31:39 -0500 Subject: Changed broken perms --- bin/nova-ajax-console-proxy | 0 bin/nova-api | 0 bin/nova-compute | 0 bin/nova-console | 0 bin/nova-dhcpbridge | 0 bin/nova-direct-api | 0 bin/nova-import-canonical-imagestore | 0 bin/nova-instancemonitor | 0 bin/nova-manage | 0 bin/nova-network | 0 bin/nova-objectstore | 0 bin/nova-scheduler | 0 bin/nova-vncproxy | 0 bin/nova-volume | 0 bin/stack | 0 doc/source/devref/down.sh | 0 doc/source/devref/up.sh | 0 nova/cloudpipe/bootscript.template | 0 plugins/xenserver/networking/etc/init.d/host-rules | 0 plugins/xenserver/networking/etc/init.d/openvswitch-nova | 0 .../networking/etc/xensource/scripts/ovs_configure_base_flows.py | 0 .../xenserver/networking/etc/xensource/scripts/ovs_configure_vif_flows.py | 0 plugins/xenserver/networking/etc/xensource/scripts/vif_rules.py | 0 plugins/xenserver/xenapi/etc/xapi.d/plugins/agent | 0 plugins/xenserver/xenapi/etc/xapi.d/plugins/pluginlib_nova.py | 0 plugins/xenserver/xenapi/etc/xapi.d/plugins/xenstore.py | 0 tools/ajaxterm/ajaxterm.py | 0 tools/ajaxterm/configure | 0 tools/clean-vlans | 0 tools/euca-get-ajax-console | 0 tools/nova-debug | 0 31 files changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 bin/nova-ajax-console-proxy mode change 100644 => 100755 bin/nova-api mode change 100644 => 100755 bin/nova-compute mode change 100644 => 100755 bin/nova-console mode change 100644 => 100755 bin/nova-dhcpbridge mode change 100644 => 100755 bin/nova-direct-api mode change 100644 => 100755 bin/nova-import-canonical-imagestore mode change 100644 => 100755 bin/nova-instancemonitor mode change 100644 => 100755 bin/nova-manage mode change 100644 => 100755 bin/nova-network mode change 100644 => 100755 bin/nova-objectstore mode change 100644 => 100755 bin/nova-scheduler mode change 100644 => 100755 bin/nova-vncproxy mode change 100644 => 100755 bin/nova-volume mode change 100644 => 100755 bin/stack mode change 100755 => 100644 doc/source/devref/down.sh mode change 100755 => 100644 doc/source/devref/up.sh mode change 100644 => 100755 nova/cloudpipe/bootscript.template mode change 100644 => 100755 plugins/xenserver/networking/etc/init.d/host-rules mode change 100644 => 100755 plugins/xenserver/networking/etc/init.d/openvswitch-nova mode change 100644 => 100755 plugins/xenserver/networking/etc/xensource/scripts/ovs_configure_base_flows.py mode change 100644 => 100755 plugins/xenserver/networking/etc/xensource/scripts/ovs_configure_vif_flows.py mode change 100644 => 100755 plugins/xenserver/networking/etc/xensource/scripts/vif_rules.py mode change 100644 => 100755 plugins/xenserver/xenapi/etc/xapi.d/plugins/agent mode change 100644 => 100755 plugins/xenserver/xenapi/etc/xapi.d/plugins/pluginlib_nova.py mode change 100644 => 100755 plugins/xenserver/xenapi/etc/xapi.d/plugins/xenstore.py mode change 100644 => 100755 tools/ajaxterm/ajaxterm.py mode change 100644 => 100755 tools/ajaxterm/configure mode change 100644 => 100755 tools/clean-vlans mode change 100644 => 100755 tools/euca-get-ajax-console mode change 100644 => 100755 tools/nova-debug diff --git a/bin/nova-ajax-console-proxy b/bin/nova-ajax-console-proxy old mode 100644 new mode 100755 diff --git a/bin/nova-api b/bin/nova-api old mode 100644 new mode 100755 diff --git a/bin/nova-compute b/bin/nova-compute old mode 100644 new mode 100755 diff --git a/bin/nova-console b/bin/nova-console old mode 100644 new mode 100755 diff --git a/bin/nova-dhcpbridge b/bin/nova-dhcpbridge old mode 100644 new mode 100755 diff --git a/bin/nova-direct-api b/bin/nova-direct-api old mode 100644 new mode 100755 diff --git a/bin/nova-import-canonical-imagestore b/bin/nova-import-canonical-imagestore old mode 100644 new mode 100755 diff --git a/bin/nova-instancemonitor b/bin/nova-instancemonitor old mode 100644 new mode 100755 diff --git a/bin/nova-manage b/bin/nova-manage old mode 100644 new mode 100755 diff --git a/bin/nova-network b/bin/nova-network old mode 100644 new mode 100755 diff --git a/bin/nova-objectstore b/bin/nova-objectstore old mode 100644 new mode 100755 diff --git a/bin/nova-scheduler b/bin/nova-scheduler old mode 100644 new mode 100755 diff --git a/bin/nova-vncproxy b/bin/nova-vncproxy old mode 100644 new mode 100755 diff --git a/bin/nova-volume b/bin/nova-volume old mode 100644 new mode 100755 diff --git a/bin/stack b/bin/stack old mode 100644 new mode 100755 diff --git a/doc/source/devref/down.sh b/doc/source/devref/down.sh old mode 100755 new mode 100644 diff --git a/doc/source/devref/up.sh b/doc/source/devref/up.sh old mode 100755 new mode 100644 diff --git a/nova/cloudpipe/bootscript.template b/nova/cloudpipe/bootscript.template old mode 100644 new mode 100755 diff --git a/plugins/xenserver/networking/etc/init.d/host-rules b/plugins/xenserver/networking/etc/init.d/host-rules old mode 100644 new mode 100755 diff --git a/plugins/xenserver/networking/etc/init.d/openvswitch-nova b/plugins/xenserver/networking/etc/init.d/openvswitch-nova old mode 100644 new mode 100755 diff --git a/plugins/xenserver/networking/etc/xensource/scripts/ovs_configure_base_flows.py b/plugins/xenserver/networking/etc/xensource/scripts/ovs_configure_base_flows.py old mode 100644 new mode 100755 diff --git a/plugins/xenserver/networking/etc/xensource/scripts/ovs_configure_vif_flows.py b/plugins/xenserver/networking/etc/xensource/scripts/ovs_configure_vif_flows.py old mode 100644 new mode 100755 diff --git a/plugins/xenserver/networking/etc/xensource/scripts/vif_rules.py b/plugins/xenserver/networking/etc/xensource/scripts/vif_rules.py old mode 100644 new mode 100755 diff --git a/plugins/xenserver/xenapi/etc/xapi.d/plugins/agent b/plugins/xenserver/xenapi/etc/xapi.d/plugins/agent old mode 100644 new mode 100755 diff --git a/plugins/xenserver/xenapi/etc/xapi.d/plugins/pluginlib_nova.py b/plugins/xenserver/xenapi/etc/xapi.d/plugins/pluginlib_nova.py old mode 100644 new mode 100755 diff --git a/plugins/xenserver/xenapi/etc/xapi.d/plugins/xenstore.py b/plugins/xenserver/xenapi/etc/xapi.d/plugins/xenstore.py old mode 100644 new mode 100755 diff --git a/tools/ajaxterm/ajaxterm.py b/tools/ajaxterm/ajaxterm.py old mode 100644 new mode 100755 diff --git a/tools/ajaxterm/configure b/tools/ajaxterm/configure old mode 100644 new mode 100755 diff --git a/tools/clean-vlans b/tools/clean-vlans old mode 100644 new mode 100755 diff --git a/tools/euca-get-ajax-console b/tools/euca-get-ajax-console old mode 100644 new mode 100755 diff --git a/tools/nova-debug b/tools/nova-debug old mode 100644 new mode 100755 -- cgit From 5ba19f7d7d9e624be5aebce496f88120ea1175e4 Mon Sep 17 00:00:00 2001 From: Trey Morris Date: Mon, 11 Jul 2011 15:49:41 -0500 Subject: catch raise for networks not found in network host and instance setup --- nova/network/manager.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/nova/network/manager.py b/nova/network/manager.py index d42bc8c4e..d7ac460ae 100644 --- a/nova/network/manager.py +++ b/nova/network/manager.py @@ -336,7 +336,12 @@ class NetworkManager(manager.SchedulerDependentManager): def set_network_hosts(self, context): """Set the network hosts for any networks which are unset.""" - networks = self.db.network_get_all(context) + try: + networks = self.db.network_get_all(context) + except Exception.NoNetworksFound: + # we don't care if no networks are found + pass + for network in networks: host = network['host'] if not host: @@ -348,7 +353,11 @@ 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 - networks = self.db.network_get_all(context) + 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 and have host set return [network for network in networks if -- cgit From 232bff13a0363a09f9b12551c4cd02787cc73702 Mon Sep 17 00:00:00 2001 From: "matt.dietz@rackspace.com" <> Date: Mon, 11 Jul 2011 16:26:46 -0500 Subject: Bad test --- nova/tests/api/openstack/test_servers.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/nova/tests/api/openstack/test_servers.py b/nova/tests/api/openstack/test_servers.py index 0afe9a8ea..775f66ad0 100644 --- a/nova/tests/api/openstack/test_servers.py +++ b/nova/tests/api/openstack/test_servers.py @@ -1463,11 +1463,10 @@ class ServersTest(test.TestCase): req = webob.Request.blank('/v1.0/servers/1/rescue') req.method = 'POST' req.content_type = 'application/json' - req.body = json.dumps(body) res = req.get_response(fakes.wsgi_app()) - self.assertEqual(res.status_int, 400) + self.assertEqual(res.status_int, 422) def test_delete_server_instance_v1_1(self): req = webob.Request.blank('/v1.1/servers/1') -- cgit From 46c59d20fd86f8ce6a57e7c663d893f6f5411d88 Mon Sep 17 00:00:00 2001 From: Sandy Walsh Date: Mon, 11 Jul 2011 18:49:46 -0700 Subject: pep8 --- nova/compute/api.py | 2 +- nova/compute/manager.py | 2 +- nova/virt/driver.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/nova/compute/api.py b/nova/compute/api.py index edc96d5ea..432658bbb 100644 --- a/nova/compute/api.py +++ b/nova/compute/api.py @@ -903,7 +903,7 @@ class API(base.Base): self._cast_compute_message('add_fixed_ip_to_instance', context, 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.""" diff --git a/nova/compute/manager.py b/nova/compute/manager.py index 3f8fc809e..c627d2985 100644 --- a/nova/compute/manager.py +++ b/nova/compute/manager.py @@ -1361,7 +1361,7 @@ class ComputeManager(manager.SchedulerDependentManager): error_list.append(ex) try: - pass #self._poll_instance_states(context) + self._poll_instance_states(context) except Exception as ex: LOG.warning(_("Error during instance poll: %s"), unicode(ex)) diff --git a/nova/virt/driver.py b/nova/virt/driver.py index b5476fcb0..178279d31 100644 --- a/nova/virt/driver.py +++ b/nova/virt/driver.py @@ -244,7 +244,7 @@ class ComputeDriver(object): def inject_network_info(self, instance, nw_info): """inject network info for specified instance""" - pass # raise NotImplementedError() + pass def poll_rescued_instances(self, timeout): """Poll for rescued instances""" -- cgit From 10a3b6c4e2ad1722ae4566f6ace997fe54769a36 Mon Sep 17 00:00:00 2001 From: Sandy Walsh Date: Mon, 11 Jul 2011 18:59:01 -0700 Subject: stray debug --- nova/api/openstack/contrib/multinic.py | 1 - 1 file changed, 1 deletion(-) diff --git a/nova/api/openstack/contrib/multinic.py b/nova/api/openstack/contrib/multinic.py index 2dd1d484e..841061721 100644 --- a/nova/api/openstack/contrib/multinic.py +++ b/nova/api/openstack/contrib/multinic.py @@ -96,7 +96,6 @@ class Multinic(extensions.ExtensionDescriptor): LOG.exception(_("Missing 'networkId' argument for addFixedIp")) return faults.Fault(exc.HTTPUnprocessableEntity()) - LOG.exception("ADD FIXED IP") # Add the fixed IP network_id = input_dict['addFixedIp']['networkId'] self.compute_api.add_fixed_ip(req.environ['nova.context'], id, -- cgit From db356e04fa79fc77a21017f2120580805a57010b Mon Sep 17 00:00:00 2001 From: Alexander Sakhnov Date: Tue, 12 Jul 2011 17:25:42 +0400 Subject: Fix 809316 bug which prevent cloudpipe to get valid IP. --- Authors | 1 + nova/network/manager.py | 11 +++++++---- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/Authors b/Authors index d2b1b627c..4aa65eea2 100644 --- a/Authors +++ b/Authors @@ -1,4 +1,5 @@ Alex Meade +Alexander Sakhnov Andrey Brindeyev Andy Smith Andy Southgate diff --git a/nova/network/manager.py b/nova/network/manager.py index d7ac460ae..7b92b61d4 100644 --- a/nova/network/manager.py +++ b/nova/network/manager.py @@ -124,7 +124,7 @@ 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, networks): + def _allocate_fixed_ips(self, context, instance_id, networks, **kwargs): """Calls allocate_fixed_ip once for each network.""" green_pool = greenpool.GreenPool() @@ -136,13 +136,14 @@ class RPCAllocateFixedIP(object): args = {} args['instance_id'] = instance_id args['network_id'] = network['id'] + args['vpn'] = kwargs.pop('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=kwargs.pop('vpn')) # wait for all of the allocates (if any) to finish green_pool.waitall() @@ -371,13 +372,14 @@ class NetworkManager(manager.SchedulerDependentManager): instance_id = kwargs.pop('instance_id') 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) networks = self._get_networks_for_instance(admin_context, instance_id, project_id) self._allocate_mac_addresses(context, instance_id, networks) - self._allocate_fixed_ips(admin_context, instance_id, networks) + self._allocate_fixed_ips(admin_context, instance_id, networks, vpn=vpn) return self.get_instance_nw_info(context, instance_id, type_id) def deallocate_for_instance(self, context, **kwargs): @@ -646,7 +648,7 @@ class NetworkManager(manager.SchedulerDependentManager): 'address': address, 'reserved': reserved}) - def _allocate_fixed_ips(self, context, instance_id, networks): + def _allocate_fixed_ips(self, context, instance_id, networks, **kwargs): """Calls allocate_fixed_ip once for each network.""" raise NotImplementedError() @@ -812,6 +814,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']) -- cgit From ee8c298e4da1f59807d9223b33a7e3bb353641a9 Mon Sep 17 00:00:00 2001 From: Alexander Sakhnov Date: Tue, 12 Jul 2011 19:47:26 +0400 Subject: Fix PEP8 for 809316 bugfix. --- nova/network/manager.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/nova/network/manager.py b/nova/network/manager.py index 7b92b61d4..21d151033 100644 --- a/nova/network/manager.py +++ b/nova/network/manager.py @@ -143,7 +143,8 @@ class RPCAllocateFixedIP(object): 'args': args}) else: # i am the correct host, run here - self.allocate_fixed_ip(context, instance_id, network, vpn=kwargs.pop('vpn')) + self.allocate_fixed_ip(context, instance_id, network, + vpn=kwargs.pop('vpn')) # wait for all of the allocates (if any) to finish green_pool.waitall() -- cgit From 495137fb383766ae5345fd8b30610a93483c0eaf Mon Sep 17 00:00:00 2001 From: Alex Meade Date: Tue, 12 Jul 2011 12:01:13 -0400 Subject: Updated remove_version_from_href to be more intelligent Added tests --- nova/api/openstack/common.py | 14 ++++++++++-- nova/tests/api/openstack/test_common.py | 39 +++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 2 deletions(-) diff --git a/nova/api/openstack/common.py b/nova/api/openstack/common.py index 9aa384f33..83854b13f 100644 --- a/nova/api/openstack/common.py +++ b/nova/api/openstack/common.py @@ -136,11 +136,21 @@ def get_id_from_href(href): raise webob.exc.HTTPBadRequest(_('could not parse id from href')) -def remove_version_from_href(base_url): +def remove_version_from_href(href): """Removes the api version from the href. Given: 'http://www.nova.com/v1.1/123' Returns: 'http://www.nova.com/123' """ - return base_url.rsplit('/', 1).pop(0) + try: + #matches /v#.# + new_href = re.sub(r'[/][v][0-9]*.[0-9]*', '', href) + if new_href == href: + msg = _('href does not contain version') + raise webob.exc.HTTPBadRequest(explanation=msg) + return new_href + except: + LOG.debug(_("Error removing version from href: %s") % href) + msg = _('could not parse version from href') + raise webob.exc.HTTPBadRequest(explanation=msg) diff --git a/nova/tests/api/openstack/test_common.py b/nova/tests/api/openstack/test_common.py index 29cb8b944..584953c3a 100644 --- a/nova/tests/api/openstack/test_common.py +++ b/nova/tests/api/openstack/test_common.py @@ -190,3 +190,42 @@ 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_bad_request(self): + + fixture = 'http://www.testsite.com/1.1/images' + self.assertRaises(webob.exc.HTTPBadRequest, + 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(webob.exc.HTTPBadRequest, + common.get_id_from_href, + fixture) + -- cgit From 50357685282b9200ccc8c82361c5266f1f413531 Mon Sep 17 00:00:00 2001 From: Alex Meade Date: Tue, 12 Jul 2011 12:11:49 -0400 Subject: pep8 --- nova/tests/api/openstack/test_common.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/nova/tests/api/openstack/test_common.py b/nova/tests/api/openstack/test_common.py index 584953c3a..1143fb502 100644 --- a/nova/tests/api/openstack/test_common.py +++ b/nova/tests/api/openstack/test_common.py @@ -195,37 +195,31 @@ class PaginationParamsTest(test.TestCase): 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_bad_request(self): - fixture = 'http://www.testsite.com/1.1/images' self.assertRaises(webob.exc.HTTPBadRequest, 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(webob.exc.HTTPBadRequest, common.get_id_from_href, fixture) - -- cgit From 5ab2fd27ea930669c01f309dbbfb794fd7c37ad6 Mon Sep 17 00:00:00 2001 From: Alex Meade Date: Tue, 12 Jul 2011 12:42:56 -0400 Subject: updated images tests --- nova/tests/api/openstack/test_images.py | 74 ++++++++++++++++++++++++++++++++- 1 file changed, 72 insertions(+), 2 deletions(-) diff --git a/nova/tests/api/openstack/test_images.py b/nova/tests/api/openstack/test_images.py index 306c0b1db..696493d4f 100644 --- a/nova/tests/api/openstack/test_images.py +++ b/nova/tests/api/openstack/test_images.py @@ -413,6 +413,14 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): "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", @@ -628,6 +636,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, @@ -657,6 +667,14 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): 'status': 'QUEUED', 'server': { 'id': 42, + "links": [{ + "rel": "self", + "href": server_href, + }, + { + "rel": "bookmark", + "href": server_bookmark, + }], }, "links": [{ "rel": "self", @@ -680,6 +698,14 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): 'progress': 0, 'server': { 'id': 42, + "links": [{ + "rel": "self", + "href": server_href, + }, + { + "rel": "bookmark", + "href": server_bookmark, + }], }, "links": [{ "rel": "self", @@ -702,6 +728,14 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): 'status': 'ACTIVE', 'server': { 'id': 42, + "links": [{ + "rel": "self", + "href": server_href, + }, + { + "rel": "bookmark", + "href": server_bookmark, + }], }, "links": [{ "rel": "self", @@ -724,6 +758,14 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): 'status': 'FAILED', 'server': { 'id': 42, + "links": [{ + "rel": "self", + "href": server_href, + }, + { + "rel": "bookmark", + "href": server_bookmark, + }], }, "links": [{ "rel": "self", @@ -1044,6 +1086,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' @@ -1052,11 +1095,25 @@ 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' @@ -1065,7 +1122,20 @@ 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_server_ref_bad_hostname(self): -- cgit From 3c8de7e99e7cd8868f63fb0d15845b2462b77b3e Mon Sep 17 00:00:00 2001 From: Alex Meade Date: Tue, 12 Jul 2011 12:46:15 -0400 Subject: Updated images viewbuilder to return links in server entity --- nova/api/openstack/views/images.py | 10 ++++++++++ nova/tests/api/openstack/test_images.py | 4 ++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/nova/api/openstack/views/images.py b/nova/api/openstack/views/images.py index d1f89a785..5c0510377 100644 --- a/nova/api/openstack/views/images.py +++ b/nova/api/openstack/views/images.py @@ -101,6 +101,16 @@ class ViewBuilderV11(ViewBuilder): 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 diff --git a/nova/tests/api/openstack/test_images.py b/nova/tests/api/openstack/test_images.py index 696493d4f..c1ead6e28 100644 --- a/nova/tests/api/openstack/test_images.py +++ b/nova/tests/api/openstack/test_images.py @@ -1096,7 +1096,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): self.assertEqual(200, response.status_int) result = json.loads(response.body) expected = { - 'id': '1', + 'id': 1, 'links': [ { 'rel': 'self', @@ -1123,7 +1123,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): self.assertEqual(200, response.status_int) result = json.loads(response.body) expected = { - 'id': '1', + 'id': 1, 'links': [ { 'rel': 'self', -- cgit From f4dc32ad0729b40ebe5765a57edff9535b992953 Mon Sep 17 00:00:00 2001 From: Alex Meade Date: Tue, 12 Jul 2011 12:55:37 -0400 Subject: Updated ImageXMLSerializer to serialize links in the server entity --- nova/api/openstack/images.py | 9 ++++----- nova/tests/api/openstack/test_images.py | 30 ++++++++++++++++++++---------- 2 files changed, 24 insertions(+), 15 deletions(-) diff --git a/nova/api/openstack/images.py b/nova/api/openstack/images.py index 873e025c7..d0317583e 100644 --- a/nova/api/openstack/images.py +++ b/nova/api/openstack/images.py @@ -325,11 +325,10 @@ class ImageXMLSerializer(wsgi.XMLDictSerializer): def _create_server_node(self, xml_doc, server): server_node = xml_doc.createElement('server') server_node.setAttribute('id', str(server['id'])) - #server_node.setAttribute('name', server['name']) - #link_nodes = self._create_link_nodes(xml_doc, - # image['links']) - #for link_node in link_nodes: - # server_node.appendChild(link_node) + 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): diff --git a/nova/tests/api/openstack/test_images.py b/nova/tests/api/openstack/test_images.py index c1ead6e28..f0d7be2b9 100644 --- a/nova/tests/api/openstack/test_images.py +++ b/nova/tests/api/openstack/test_images.py @@ -1225,7 +1225,6 @@ class ImageXMLSerializationTest(test.TestCase): 'progress': 80, 'server': { 'id': 1, - 'name': 'Server1', 'links': [ { 'href': self.SERVER_HREF, @@ -1270,7 +1269,10 @@ class ImageXMLSerializationTest(test.TestCase): created="%(expected_now)s" status="ACTIVE" progress="80"> - + + + + value1 @@ -1296,7 +1298,6 @@ class ImageXMLSerializationTest(test.TestCase): 'status': 'ACTIVE', 'server': { 'id': 1, - 'name': 'Server1', 'links': [ { 'href': self.SERVER_HREF, @@ -1338,7 +1339,10 @@ class ImageXMLSerializationTest(test.TestCase): updated="%(expected_now)s" created="%(expected_now)s" status="ACTIVE"> - + + + + @@ -1359,7 +1363,6 @@ class ImageXMLSerializationTest(test.TestCase): 'status': 'ACTIVE', 'server': { 'id': 1, - 'name': 'Server1', 'links': [ { 'href': self.SERVER_HREF, @@ -1400,7 +1403,10 @@ class ImageXMLSerializationTest(test.TestCase): updated="%(expected_now)s" created="%(expected_now)s" status="ACTIVE"> - + + + + @@ -1550,7 +1556,6 @@ class ImageXMLSerializationTest(test.TestCase): 'status': 'ACTIVE', 'server': { 'id': 1, - 'name': 'Server1', 'links': [ { 'href': self.SERVER_HREF, @@ -1616,7 +1621,10 @@ class ImageXMLSerializationTest(test.TestCase): updated="%(expected_now)s" created="%(expected_now)s" status="ACTIVE"> - + + + + @@ -1654,7 +1662,6 @@ class ImageXMLSerializationTest(test.TestCase): 'progress': 80, 'server': { 'id': 1, - 'name': 'Server1', 'links': [ { 'href': self.SERVER_HREF, @@ -1699,7 +1706,10 @@ class ImageXMLSerializationTest(test.TestCase): created="%(expected_now)s" status="SAVING" progress="80"> - + + + + value1 -- cgit From 7a700362d63de1da51e9a890d854c3b0eeb97aae Mon Sep 17 00:00:00 2001 From: Alex Meade Date: Tue, 12 Jul 2011 13:29:26 -0400 Subject: minor cleanup --- nova/api/openstack/wsgi.py | 4 +++- nova/tests/api/openstack/test_images.py | 12 ------------ 2 files changed, 3 insertions(+), 13 deletions(-) diff --git a/nova/api/openstack/wsgi.py b/nova/api/openstack/wsgi.py index 4e4ce4121..c3f841aa5 100644 --- a/nova/api/openstack/wsgi.py +++ b/nova/api/openstack/wsgi.py @@ -277,7 +277,9 @@ class XMLDictSerializer(DictSerializer): return node.toprettyxml(indent=' ', encoding='UTF-8') #NOTE (ameade): the has_atom should be removed after all of the - # xml serializers and view builders have been updated + # 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) diff --git a/nova/tests/api/openstack/test_images.py b/nova/tests/api/openstack/test_images.py index f0d7be2b9..716b73f6c 100644 --- a/nova/tests/api/openstack/test_images.py +++ b/nova/tests/api/openstack/test_images.py @@ -1213,8 +1213,6 @@ class ImageXMLSerializationTest(test.TestCase): def test_show(self): serializer = images.ImageXMLSerializer() - #so we can see the full diff in the output - self.maxDiff = None fixture = { 'image': { 'id': 1, @@ -1288,7 +1286,6 @@ class ImageXMLSerializationTest(test.TestCase): def test_show_zero_metadata(self): serializer = images.ImageXMLSerializer() - self.maxDiff = None fixture = { 'image': { 'id': 1, @@ -1353,7 +1350,6 @@ class ImageXMLSerializationTest(test.TestCase): def test_show_image_no_metadata_key(self): serializer = images.ImageXMLSerializer() - self.maxDiff = None fixture = { 'image': { 'id': 1, @@ -1417,8 +1413,6 @@ class ImageXMLSerializationTest(test.TestCase): def test_show_no_server(self): serializer = images.ImageXMLSerializer() - #so we can see the full diff in the output - self.maxDiff = None fixture = { 'image': { 'id': 1, @@ -1471,8 +1465,6 @@ class ImageXMLSerializationTest(test.TestCase): def test_index(self): serializer = images.ImageXMLSerializer() - #so we can see the full diff in the output - self.maxDiff = None fixture = { 'images': [ { @@ -1544,8 +1536,6 @@ class ImageXMLSerializationTest(test.TestCase): def test_detail(self): serializer = images.ImageXMLSerializer() - #so we can see the full diff in the output - self.maxDiff = None fixture = { 'images': [ { @@ -1650,8 +1640,6 @@ class ImageXMLSerializationTest(test.TestCase): def test_create(self): serializer = images.ImageXMLSerializer() - #so we can see the full diff in the output - self.maxDiff = None fixture = { 'image': { 'id': 1, -- cgit From 915c7d52fa1b2afe6af9686210982d5bc043be97 Mon Sep 17 00:00:00 2001 From: Mike Scherbakov Date: Tue, 12 Jul 2011 11:07:30 -0700 Subject: Renamed 'nova-manage server list' -> 'nova-manage host list' to differentiate physical hosts from VMs --- bin/nova-manage | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/bin/nova-manage b/bin/nova-manage index 8ba78a56a..94ab22092 100755 --- a/bin/nova-manage +++ b/bin/nova-manage @@ -816,11 +816,11 @@ class ServiceCommands(object): {"method": "update_available_resource"}) -class ServerCommands(object): - """List servers""" +class HostCommands(object): + """List hosts""" def list(self, zone=None): - """Show a list of all servers. Filter by zone. + """Show a list of all physical hosts. Filter by zone. args: [zone]""" print "%-25s\t%-15s" % (_('host'), _('zone')) @@ -829,13 +829,13 @@ class ServerCommands(object): services = db.service_get_all(ctxt) if zone: services = [s for s in services if s['availability_zone'] == zone] - servers = [] + hosts = [] for srv in services: - if not [s for s in servers if s['host'] == srv['host']]: - servers.append(srv) + if not [h for h in hosts if h['host'] == srv['host']]: + hosts.append(srv) - for srv in servers: - print "%-25s\t%-15s" % (srv['host'], srv['availability_zone']) + for h in hosts: + print "%-25s\t%-15s" % (h['host'], h['availability_zone']) class DbCommands(object): @@ -1211,13 +1211,13 @@ CATEGORIES = [ ('fixed', FixedIpCommands), ('flavor', InstanceTypeCommands), ('floating', FloatingIpCommands), + ('host', HostCommands), ('instance_type', InstanceTypeCommands), ('image', ImageCommands), ('network', NetworkCommands), ('project', ProjectCommands), ('role', RoleCommands), ('service', ServiceCommands), - ('server', ServerCommands), ('shell', ShellCommands), ('user', UserCommands), ('version', VersionCommands), -- cgit From c085294ccb6a8d449ccfd5739be67ee12538f48f Mon Sep 17 00:00:00 2001 From: Alex Meade Date: Tue, 12 Jul 2011 14:19:30 -0400 Subject: Updated some common.py functions to raise ValueErrors instead of HTTPBadRequests --- nova/api/openstack/common.py | 13 +++++++------ nova/tests/api/openstack/test_common.py | 4 ++-- nova/tests/api/openstack/test_images.py | 22 ++++++++++++++++++++++ 3 files changed, 31 insertions(+), 8 deletions(-) diff --git a/nova/api/openstack/common.py b/nova/api/openstack/common.py index 83854b13f..79969d393 100644 --- a/nova/api/openstack/common.py +++ b/nova/api/openstack/common.py @@ -133,7 +133,7 @@ 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(href): @@ -146,11 +146,12 @@ def remove_version_from_href(href): try: #matches /v#.# new_href = re.sub(r'[/][v][0-9]*.[0-9]*', '', href) - if new_href == href: - msg = _('href does not contain version') - raise webob.exc.HTTPBadRequest(explanation=msg) - return new_href except: LOG.debug(_("Error removing version from href: %s") % href) msg = _('could not parse version from href') - raise webob.exc.HTTPBadRequest(explanation=msg) + raise ValueError(msg) + + if new_href == href: + msg = _('href does not contain version') + raise ValueError(msg) + return new_href diff --git a/nova/tests/api/openstack/test_common.py b/nova/tests/api/openstack/test_common.py index 1143fb502..7440bccfb 100644 --- a/nova/tests/api/openstack/test_common.py +++ b/nova/tests/api/openstack/test_common.py @@ -208,7 +208,7 @@ class MiscFunctionsTest(test.TestCase): def test_remove_version_from_href_bad_request(self): fixture = 'http://www.testsite.com/1.1/images' - self.assertRaises(webob.exc.HTTPBadRequest, + self.assertRaises(ValueError, common.remove_version_from_href, fixture) @@ -220,6 +220,6 @@ class MiscFunctionsTest(test.TestCase): def test_get_id_from_href_bad_request(self): fixture = 'http://45' - self.assertRaises(webob.exc.HTTPBadRequest, + 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 716b73f6c..c1bdd6906 100644 --- a/nova/tests/api/openstack/test_images.py +++ b/nova/tests/api/openstack/test_images.py @@ -1158,6 +1158,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 -- cgit From 952196a533c3577945a2c4245436d09eb75e0eb6 Mon Sep 17 00:00:00 2001 From: Mohammed Naser Date: Tue, 12 Jul 2011 19:12:21 -0400 Subject: Made sure the network manager accepts kwargs for FlatManager --- nova/network/manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nova/network/manager.py b/nova/network/manager.py index 21d151033..383fbd32f 100644 --- a/nova/network/manager.py +++ b/nova/network/manager.py @@ -696,7 +696,7 @@ class FlatManager(NetworkManager): timeout_fixed_ips = False - def _allocate_fixed_ips(self, context, instance_id, networks): + def _allocate_fixed_ips(self, context, instance_id, networks, **kwargs): """Calls allocate_fixed_ip once for each network.""" for network in networks: self.allocate_fixed_ip(context, instance_id, network) -- cgit From 7acb3d63cd2c487c78994e15f1e015d3b81febf8 Mon Sep 17 00:00:00 2001 From: Dan Prince Date: Tue, 12 Jul 2011 21:50:12 -0400 Subject: Don't pop 'vpn' on kwargs inside a loop in RPCAllocateFixedIP._allocate_fixed_ips (fixes KeyError's). Fix allocate_fixed_ip method signature for FlatDHCP. --- nova/network/manager.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/nova/network/manager.py b/nova/network/manager.py index 383fbd32f..007766998 100644 --- a/nova/network/manager.py +++ b/nova/network/manager.py @@ -128,6 +128,7 @@ class RPCAllocateFixedIP(object): """Calls allocate_fixed_ip once for each network.""" green_pool = greenpool.GreenPool() + vpn = kwargs.pop('vpn') for network in networks: if network['host'] != self.host: # need to call allocate_fixed_ip to correct network host @@ -136,15 +137,14 @@ class RPCAllocateFixedIP(object): args = {} args['instance_id'] = instance_id args['network_id'] = network['id'] - args['vpn'] = kwargs.pop('vpn') + 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, - vpn=kwargs.pop('vpn')) + self.allocate_fixed_ip(context, instance_id, network, vpn=vpn) # wait for all of the allocates (if any) to finish green_pool.waitall() @@ -753,7 +753,7 @@ class FlatDHCPManager(FloatingIP, RPCAllocateFixedIP, NetworkManager): self.driver.ensure_bridge(network['bridge'], network['bridge_interface']) - def allocate_fixed_ip(self, context, instance_id, network): + 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, -- cgit From f794139e6ad70949bdaf26417989f3940e8af3b7 Mon Sep 17 00:00:00 2001 From: Dan Prince Date: Wed, 13 Jul 2011 08:34:41 -0400 Subject: Added Mohammed Naser to Authors file. --- Authors | 1 + 1 file changed, 1 insertion(+) diff --git a/Authors b/Authors index 4aa65eea2..2e50cfbe0 100644 --- a/Authors +++ b/Authors @@ -65,6 +65,7 @@ Masanori Itoh Matt Dietz Michael Gundlach Mike Scherbakov +Mohammed Naser Monsyne Dragon Monty Taylor MORITA Kazutaka -- cgit From 3a89f16ea07ebfc3d2c4e08ff9072b5020a5d348 Mon Sep 17 00:00:00 2001 From: Anne Gentle Date: Wed, 13 Jul 2011 09:04:20 -0500 Subject: Add multinic doc and distributed scheduler doc to developer guide front page --- doc/source/devref/index.rst | 7 +++++-- doc/source/devref/multinic.rst | 4 ++-- 2 files changed, 7 insertions(+), 4 deletions(-) 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. -- cgit From 880121c8498530d9c0e9a38e983c4d4518c1189e Mon Sep 17 00:00:00 2001 From: Brian Waldon Date: Wed, 13 Jul 2011 11:10:40 -0400 Subject: adding test; casting instance to dict to prevent sqlalchemy errors --- nova/api/openstack/__init__.py | 13 ++++--------- nova/api/openstack/ips.py | 2 +- nova/api/openstack/servers.py | 5 +++-- nova/db/sqlalchemy/api.py | 1 + nova/tests/api/openstack/test_servers.py | 16 ++++++++++++++++ 5 files changed, 25 insertions(+), 12 deletions(-) diff --git a/nova/api/openstack/__init__.py b/nova/api/openstack/__init__.py index 3e4d87ee0..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'}) @@ -154,11 +158,6 @@ class APIRouterV10(APIRouter): parent_resource=dict(member_name='server', collection_name='servers')) - mapper.resource("ip", "ips", controller=ips.create_resource('1.0'), - member=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.""" @@ -174,7 +173,3 @@ class APIRouterV11(APIRouter): controller=server_metadata.create_resource(), parent_resource=dict(member_name='server', collection_name='servers')) - - mapper.resource("ip", "ips", controller=ips.create_resource('1.1'), - parent_resource=dict(member_name='server', - collection_name='servers')) diff --git a/nova/api/openstack/ips.py b/nova/api/openstack/ips.py index b83e2f9b3..59e3c084c 100644 --- a/nova/api/openstack/ips.py +++ b/nova/api/openstack/ips.py @@ -93,7 +93,7 @@ class ControllerV11(Controller): def _get_virtual_interfaces(self, context, server_id): try: return db.api.virtual_interface_get_by_instance(context, server_id) - except exception.InstanceNotFound: + except nova.exception.InstanceNotFound: msg = _("Instance does not exist") raise exc.HTTPNotFound(explanation=msg) diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index 3c29c2606..2cb27472a 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -492,9 +492,10 @@ class ControllerV11(Controller): context = req.environ['nova.context'] interfaces = db.api.virtual_interface_get_by_instance(context, instance['id']) - instance['virtual_interfaces'] = interfaces + _instance = dict(instance) + _instance['virtual_interfaces'] = interfaces - return builder.build(instance, is_detail=is_detail) + return builder.build(_instance, is_detail=is_detail) def _action_change_password(self, input_dict, req, id): context = req.environ['nova.context'] diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index 854b7dea7..95a34afa1 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -128,6 +128,7 @@ def require_instance_exists(f): 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 diff --git a/nova/tests/api/openstack/test_servers.py b/nova/tests/api/openstack/test_servers.py index 6ee87830a..da2925798 100644 --- a/nova/tests/api/openstack/test_servers.py +++ b/nova/tests/api/openstack/test_servers.py @@ -71,6 +71,12 @@ def return_virtual_interface_by_instance(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, @@ -552,6 +558,16 @@ class ServersTest(test.TestCase): 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') res = req.get_response(fakes.wsgi_app()) -- cgit From 132a47611b2fdbbb1e6c70a33bfd092854ea6e98 Mon Sep 17 00:00:00 2001 From: Brian Waldon Date: Wed, 13 Jul 2011 14:02:44 -0400 Subject: updating testing; simplifying instance-level code --- nova/api/openstack/servers.py | 8 +------- nova/api/openstack/views/addresses.py | 1 - nova/api/openstack/views/servers.py | 2 +- nova/db/sqlalchemy/api.py | 2 +- nova/tests/api/openstack/test_servers.py | 24 +++++++++++++++--------- 5 files changed, 18 insertions(+), 19 deletions(-) diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index 2cb27472a..5230f4d5c 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -489,13 +489,7 @@ class ControllerV11(Controller): builder = nova.api.openstack.views.servers.ViewBuilderV11( addresses_builder, flavor_builder, image_builder, base_url) - context = req.environ['nova.context'] - interfaces = db.api.virtual_interface_get_by_instance(context, - instance['id']) - _instance = dict(instance) - _instance['virtual_interfaces'] = interfaces - - return builder.build(_instance, is_detail=is_detail) + return builder.build(instance, is_detail=is_detail) def _action_change_password(self, input_dict, req, id): context = req.environ['nova.context'] diff --git a/nova/api/openstack/views/addresses.py b/nova/api/openstack/views/addresses.py index c7464a7c5..b127ac38c 100644 --- a/nova/api/openstack/views/addresses.py +++ b/nova/api/openstack/views/addresses.py @@ -67,4 +67,3 @@ class ViewBuilderV11(ViewBuilder): ip = {'addr': fixed_ip['address'], 'version': 4} fixed_ips.append(ip) return fixed_ips - diff --git a/nova/api/openstack/views/servers.py b/nova/api/openstack/views/servers.py index 691cc48ca..a957aa58e 100644 --- a/nova/api/openstack/views/servers.py +++ b/nova/api/openstack/views/servers.py @@ -159,7 +159,7 @@ class ViewBuilderV11(ViewBuilder): response["flavorRef"] = flavor_ref def _build_addresses(self, response, inst): - interfaces = inst['virtual_interfaces'] + interfaces = inst.get('virtual_interfaces', []) response['addresses'] = self.addresses_builder.build(interfaces) def _build_extra(self, response, inst): diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index 95a34afa1..a831516a8 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -132,9 +132,9 @@ def require_instance_exists(f): return wrapper - ################### + @require_admin_context def service_destroy(context, service_id): session = get_session() diff --git a/nova/tests/api/openstack/test_servers.py b/nova/tests/api/openstack/test_servers.py index da2925798..7a9a89d1a 100644 --- a/nova/tests/api/openstack/test_servers.py +++ b/nova/tests/api/openstack/test_servers.py @@ -84,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) @@ -136,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: @@ -183,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, @@ -445,12 +455,8 @@ class ServersTest(test.TestCase): ], }, ] - - _return_vifs = return_virtual_interface_by_instance(interfaces) - self.stubs.Set(nova.db.api, - 'virtual_interface_get_by_instance', - _return_vifs) - + 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()) @@ -932,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() -- cgit From 6daf6d30dfeab459a0b672d909b115b1a5ce86c3 Mon Sep 17 00:00:00 2001 From: "Kevin L. Mitchell" Date: Wed, 13 Jul 2011 14:41:13 -0500 Subject: Augment rate limiting to allow greater flexibility through the api-paste.ini configuration --- nova/api/openstack/limits.py | 96 +++++++++++++++++++++++++++++++-- nova/tests/api/openstack/test_limits.py | 30 +++++++++-- 2 files changed, 116 insertions(+), 10 deletions(-) diff --git a/nova/api/openstack/limits.py b/nova/api/openstack/limits.py index d08287f6b..8642a28f9 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 @@ -119,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`. @@ -224,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): @@ -271,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`. @@ -280,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. @@ -305,6 +327,59 @@ class Limiter(object): return None, None + @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[0] != '(' or group[-1] != ')': + raise ValueError("Groups 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("Groups must contain exactly 5 elements") + + # 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): """ @@ -388,3 +463,14 @@ class WsgiLimiterProxy(object): return None, None return resp.getheader("X-Wait-Seconds"), resp.read() or None + + @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/tests/api/openstack/test_limits.py b/nova/tests/api/openstack/test_limits.py index 38c959fae..eeacd2fca 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.""" @@ -500,7 +508,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 +614,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 +634,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 -- cgit From b9ea0f43c186d5fbc232b0ddd11c3e64898136ab Mon Sep 17 00:00:00 2001 From: Scott Moser Date: Wed, 13 Jul 2011 17:16:27 -0400 Subject: support '-' to indicate stdout in nova-manage project 'environment' and 'zip' This just adds support to do: nova-manage project zip test-project admin - > out.zip nova-manage project environment test-project admin - | grep NOVA_URL --- bin/nova-manage | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/bin/nova-manage b/bin/nova-manage index 7dfe91698..b5247f8c3 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 == "-": + f = sys.stdout + else: + f = open(filename, 'w') + 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 == "-": + f = sys.stdout + else: + f = open(filename, 'w') + f.write(zip_file) except (exception.UserNotFound, exception.ProjectNotFound) as ex: print ex raise -- cgit From 7ed74cb999483e589e186944cb9116f507dbe60d Mon Sep 17 00:00:00 2001 From: Scott Moser Date: Wed, 13 Jul 2011 22:01:52 -0400 Subject: use 'with' so that close is called on file handle --- bin/nova-manage | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/bin/nova-manage b/bin/nova-manage index b5247f8c3..a83fe70f8 100755 --- a/bin/nova-manage +++ b/bin/nova-manage @@ -415,10 +415,10 @@ class ProjectCommands(object): print ex raise if filename == "-": - f = sys.stdout + sys.stdout.write(rc) else: - f = open(filename, 'w') - f.write(rc) + with open(filename, 'w') as f: + f.write(rc) def list(self, username=None): """Lists all projects @@ -469,10 +469,10 @@ class ProjectCommands(object): try: zip_file = self.manager.get_credentials(user_id, project_id) if filename == "-": - f = sys.stdout + sys.stdout.write(zip_file) else: - f = open(filename, 'w') - f.write(zip_file) + with open(filename, 'w') as f: + f.write(zip_file) except (exception.UserNotFound, exception.ProjectNotFound) as ex: print ex raise -- cgit From ee143766a32486664d47aee11f792854cbedd4ff Mon Sep 17 00:00:00 2001 From: Scott Moser Date: Thu, 14 Jul 2011 07:08:02 -0400 Subject: add self to authors --- Authors | 1 + 1 file changed, 1 insertion(+) diff --git a/Authors b/Authors index 2e50cfbe0..a366cec87 100644 --- a/Authors +++ b/Authors @@ -85,6 +85,7 @@ Ryan Lucio Salvatore Orlando Sandy Walsh Sateesh Chodapuneedi +Scott Moser Soren Hansen Thierry Carrez Todd Willey -- cgit From 1539866314393e8565eef05f1f63dba9ffa69de3 Mon Sep 17 00:00:00 2001 From: Brian Waldon Date: Thu, 14 Jul 2011 12:03:06 -0400 Subject: adding bookmark to images index --- nova/api/openstack/views/images.py | 18 +++++++++++------- nova/tests/api/openstack/test_images.py | 14 ++++++++++---- 2 files changed, 21 insertions(+), 11 deletions(-) diff --git a/nova/api/openstack/views/images.py b/nova/api/openstack/views/images.py index 5c0510377..873ce212a 100644 --- a/nova/api/openstack/views/images.py +++ b/nova/api/openstack/views/images.py @@ -121,16 +121,20 @@ class ViewBuilderV11(ViewBuilder): href = self.generate_href(image_obj["id"]) bookmark = self.generate_bookmark(image_obj["id"]) - image["links"] = [{ - "rel": "self", - "href": href, - }] + image["links"] = [ + { + "rel": "self", + "href": href, + }, + { + "rel": "bookmark", + "href": bookmark, + }, + + ] if detail: image["metadata"] = image_obj.get("properties", {}) - image["links"].append({"rel": "bookmark", - "href": bookmark, - }) return image diff --git a/nova/tests/api/openstack/test_images.py b/nova/tests/api/openstack/test_images.py index c1bdd6906..534460d46 100644 --- a/nova/tests/api/openstack/test_images.py +++ b/nova/tests/api/openstack/test_images.py @@ -568,10 +568,16 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): test_image = { "id": image["id"], "name": image["name"], - "links": [{ - "rel": "self", - "href": href, - }], + "links": [ + { + "rel": "self", + "href": href, + }, + { + "rel": "bookmark", + "href": bookmark, + }, + ], } self.assertTrue(test_image in response_list) -- cgit From cbf05e0b6351c9577e7e992da072d190c8c9a592 Mon Sep 17 00:00:00 2001 From: "Kevin L. Mitchell" Date: Thu, 14 Jul 2011 11:37:32 -0500 Subject: Comment on parse_limits(); expand an exception message; add unit tests; fix a minor discovered bug --- nova/api/openstack/limits.py | 18 ++++++-- nova/tests/api/openstack/test_limits.py | 81 +++++++++++++++++++++++++++++++++ 2 files changed, 96 insertions(+), 3 deletions(-) diff --git a/nova/api/openstack/limits.py b/nova/api/openstack/limits.py index 8642a28f9..bc76547d8 100644 --- a/nova/api/openstack/limits.py +++ b/nova/api/openstack/limits.py @@ -327,6 +327,11 @@ 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): """ @@ -351,14 +356,16 @@ class Limiter(object): result = [] for group in limits.split(';'): group = group.strip() - if group[0] != '(' or group[-1] != ')': - raise ValueError("Groups must be surrounded by parentheses") + 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("Groups must contain exactly 5 elements") + raise ValueError("Limit rules must contain the following " + "arguments: verb, uri, regex, value, unit") # Pull out the arguments verb, uri, regex, value, unit = args @@ -464,6 +471,11 @@ class WsgiLimiterProxy(object): 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): """ diff --git a/nova/tests/api/openstack/test_limits.py b/nova/tests/api/openstack/test_limits.py index eeacd2fca..e0368d5f5 100644 --- a/nova/tests/api/openstack/test_limits.py +++ b/nova/tests/api/openstack/test_limits.py @@ -500,6 +500,87 @@ 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.""" + try: + limits.Limiter.parse_limits(';;;;;') + except ValueError: + return + assert False, "Failure to reject invalid input" + + def test_bad_rule(self): + """Test that parse_limits() handles bad rules correctly.""" + try: + limits.Limiter.parse_limits('GET, *, .*, 20, minute') + except ValueError: + return + assert False, "Failure to reject bad rule" + + def test_missing_arg(self): + """Test that parse_limits() handles missing args correctly.""" + try: + limits.Limiter.parse_limits('(GET, *, .*, 20)') + except ValueError: + return + assert False, "Failure to reject missing rule argument" + + def test_bad_value(self): + """Test that parse_limits() handles bad values correctly.""" + try: + limits.Limiter.parse_limits('(GET, *, .*, foo, minute)') + except ValueError: + return + assert False, "Failure to reject invalid value" + + def test_bad_unit(self): + """Test that parse_limits() handles bad units correctly.""" + try: + limits.Limiter.parse_limits('(GET, *, .*, 20, lightyears)') + except ValueError: + return + assert False, "Failure to reject invalid unit" + + 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. -- cgit From 27b8d75f9b666ce08472270b38685d8e36a612d8 Mon Sep 17 00:00:00 2001 From: Brian Waldon Date: Thu, 14 Jul 2011 13:25:05 -0400 Subject: fixing bad merge --- nova/api/openstack/ips.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nova/api/openstack/ips.py b/nova/api/openstack/ips.py index f08d8023e..1ebfdb831 100644 --- a/nova/api/openstack/ips.py +++ b/nova/api/openstack/ips.py @@ -125,4 +125,4 @@ def create_resource(version): } serializer = wsgi.ResponseSerializer(body_serializers) - return wsgi.Resource(Controller(), serializer=serializer) + return wsgi.Resource(controller, serializer=serializer) -- cgit From ac6dcb5a8802d53390584fcde8cba8ca74c1d0d0 Mon Sep 17 00:00:00 2001 From: "Kevin L. Mitchell" Date: Thu, 14 Jul 2011 12:40:38 -0500 Subject: Use assertRaises instead of try/except--stupid brain-o --- nova/tests/api/openstack/test_limits.py | 35 ++++++++++----------------------- 1 file changed, 10 insertions(+), 25 deletions(-) diff --git a/nova/tests/api/openstack/test_limits.py b/nova/tests/api/openstack/test_limits.py index e0368d5f5..76363450d 100644 --- a/nova/tests/api/openstack/test_limits.py +++ b/nova/tests/api/openstack/test_limits.py @@ -508,43 +508,28 @@ class ParseLimitsTest(BaseLimitTestSuite): def test_invalid(self): """Test that parse_limits() handles invalid input correctly.""" - try: - limits.Limiter.parse_limits(';;;;;') - except ValueError: - return - assert False, "Failure to reject invalid input" + self.assertRaises(ValueError, limits.Limiter.parse_limits, + ';;;;;') def test_bad_rule(self): """Test that parse_limits() handles bad rules correctly.""" - try: - limits.Limiter.parse_limits('GET, *, .*, 20, minute') - except ValueError: - return - assert False, "Failure to reject bad rule" + self.assertRaises(ValueError, limits.Limiter.parse_limits, + 'GET, *, .*, 20, minute') def test_missing_arg(self): """Test that parse_limits() handles missing args correctly.""" - try: - limits.Limiter.parse_limits('(GET, *, .*, 20)') - except ValueError: - return - assert False, "Failure to reject missing rule argument" + self.assertRaises(ValueError, limits.Limiter.parse_limits, + '(GET, *, .*, 20)') def test_bad_value(self): """Test that parse_limits() handles bad values correctly.""" - try: - limits.Limiter.parse_limits('(GET, *, .*, foo, minute)') - except ValueError: - return - assert False, "Failure to reject invalid value" + self.assertRaises(ValueError, limits.Limiter.parse_limits, + '(GET, *, .*, foo, minute)') def test_bad_unit(self): """Test that parse_limits() handles bad units correctly.""" - try: - limits.Limiter.parse_limits('(GET, *, .*, 20, lightyears)') - except ValueError: - return - assert False, "Failure to reject invalid unit" + self.assertRaises(ValueError, limits.Limiter.parse_limits, + '(GET, *, .*, 20, lightyears)') def test_multiple_rules(self): """Test that parse_limits() handles multiple rules correctly.""" -- cgit From 6c3a8e16fb1347b0b5ecfc1850a4de5807004a2e Mon Sep 17 00:00:00 2001 From: Dan Prince Date: Thu, 14 Jul 2011 13:44:56 -0400 Subject: Update the agent plugin so that it gets 'b64_contents' from the args dict instead of 'b64_file' (which isn't what nova sends). --- plugins/xenserver/xenapi/etc/xapi.d/plugins/agent | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/xenserver/xenapi/etc/xapi.d/plugins/agent b/plugins/xenserver/xenapi/etc/xapi.d/plugins/agent index b8a1b936a..68d7e7bff 100755 --- a/plugins/xenserver/xenapi/etc/xapi.d/plugins/agent +++ b/plugins/xenserver/xenapi/etc/xapi.d/plugins/agent @@ -127,7 +127,7 @@ 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"): # New version of the agent. Agent should receive a 'value' -- cgit From 38233d72aff36fbdb0fd49755458b7b5100366e1 Mon Sep 17 00:00:00 2001 From: Brian Waldon Date: Thu, 14 Jul 2011 14:23:23 -0400 Subject: exposing floating ips --- nova/api/openstack/views/addresses.py | 22 ++++++++++++---------- nova/tests/api/openstack/test_servers.py | 16 ++++++++++++++-- 2 files changed, 26 insertions(+), 12 deletions(-) diff --git a/nova/api/openstack/views/addresses.py b/nova/api/openstack/views/addresses.py index b127ac38c..a242efa45 100644 --- a/nova/api/openstack/views/addresses.py +++ b/nova/api/openstack/views/addresses.py @@ -46,24 +46,26 @@ class ViewBuilderV11(ViewBuilder): networks = {} for interface in interfaces: network_label = interface['network']['label'] + if network_label not in networks: networks[network_label] = [] - for fixed_ip in interface['fixed_ips']: - ip = {'addr': fixed_ip['address'], 'version': 4} - networks[network_label].append(ip) + 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_fixed_ips(interface) - return {network_label: ips} + ips = self._extract_ipv4(interface) + return {network_label: list(ips)} return None - def _extract_fixed_ips(self, interface): - fixed_ips = [] + def _extract_ipv4(self, interface): for fixed_ip in interface['fixed_ips']: - ip = {'addr': fixed_ip['address'], 'version': 4} - fixed_ips.append(ip) - return 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/tests/api/openstack/test_servers.py b/nova/tests/api/openstack/test_servers.py index 7a2904520..3c48a2f81 100644 --- a/nova/tests/api/openstack/test_servers.py +++ b/nova/tests/api/openstack/test_servers.py @@ -484,7 +484,12 @@ class ServersTest(test.TestCase): { 'network': {'label': 'network_2'}, 'fixed_ips': [ - {'address': '172.19.0.1'}, + { + 'address': '172.19.0.1', + 'floating_ips': [ + {'address': '1.2.3.4'}, + ], + }, {'address': '172.19.0.2'}, ], }, @@ -507,6 +512,7 @@ class ServersTest(test.TestCase): ], 'network_2': [ {'version': 4, 'addr': '172.19.0.1'}, + {'version': 4, 'addr': '1.2.3.4'}, {'version': 4, 'addr': '172.19.0.2'}, ], }, @@ -526,7 +532,12 @@ class ServersTest(test.TestCase): { 'network': {'label': 'network_2'}, 'fixed_ips': [ - {'address': '172.19.0.1'}, + { + 'address': '172.19.0.1', + 'floating_ips': [ + {'address': '1.2.3.4'}, + ], + }, {'address': '172.19.0.2'}, ], }, @@ -543,6 +554,7 @@ class ServersTest(test.TestCase): expected = { 'network_2': [ {'version': 4, 'addr': '172.19.0.1'}, + {'version': 4, 'addr': '1.2.3.4'}, {'version': 4, 'addr': '172.19.0.2'}, ], } -- cgit From 7d3859833d0647cad15c614e816df9440bb99d44 Mon Sep 17 00:00:00 2001 From: Trey Morris Date: Thu, 14 Jul 2011 16:52:27 -0500 Subject: catching the correct exception --- nova/network/manager.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nova/network/manager.py b/nova/network/manager.py index 824e8d24d..24736f53d 100644 --- a/nova/network/manager.py +++ b/nova/network/manager.py @@ -340,7 +340,7 @@ class NetworkManager(manager.SchedulerDependentManager): """Set the network hosts for any networks which are unset.""" try: networks = self.db.network_get_all(context) - except Exception.NoNetworksFound: + except exception.NoNetworksFound: # we don't care if no networks are found pass @@ -357,7 +357,7 @@ class NetworkManager(manager.SchedulerDependentManager): # a non-vlan instance should connect to try: networks = self.db.network_get_all(context) - except Exception.NoNetworksFound: + except exception.NoNetworksFound: # we don't care if no networks are found pass -- cgit From dd78adbcf0b85f9473b5240af3366fb1dc2d4133 Mon Sep 17 00:00:00 2001 From: Stephanie Reese Date: Thu, 14 Jul 2011 23:09:28 -0400 Subject: Fixed remove_version_from_href Added tests --- nova/api/openstack/common.py | 13 ++++++++++--- nova/tests/api/openstack/test_common.py | 24 ++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/nova/api/openstack/common.py b/nova/api/openstack/common.py index 79969d393..8e12ce0c0 100644 --- a/nova/api/openstack/common.py +++ b/nova/api/openstack/common.py @@ -137,15 +137,22 @@ def get_id_from_href(href): def remove_version_from_href(href): - """Removes the api version from the 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' + """ try: - #matches /v#.# - new_href = re.sub(r'[/][v][0-9]*.[0-9]*', '', href) + #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') diff --git a/nova/tests/api/openstack/test_common.py b/nova/tests/api/openstack/test_common.py index 7440bccfb..4c4d03995 100644 --- a/nova/tests/api/openstack/test_common.py +++ b/nova/tests/api/openstack/test_common.py @@ -206,12 +206,36 @@ class MiscFunctionsTest(test.TestCase): 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) -- cgit From 0aeec37c27e91d031ef53eeec9952c4f470990a1 Mon Sep 17 00:00:00 2001 From: Stephanie Reese Date: Thu, 14 Jul 2011 23:12:42 -0400 Subject: Updated Authors --- Authors | 1 + 1 file changed, 1 insertion(+) diff --git a/Authors b/Authors index 2e50cfbe0..62edd6771 100644 --- a/Authors +++ b/Authors @@ -86,6 +86,7 @@ Salvatore Orlando Sandy Walsh Sateesh Chodapuneedi Soren Hansen +Stephanie Reese Thierry Carrez Todd Willey Trey Morris -- cgit From a48041f7b587b91413a138264e0ec31ba4dcc78a Mon Sep 17 00:00:00 2001 From: Naveed Massjouni Date: Fri, 15 Jul 2011 04:33:20 -0400 Subject: Adding unit and integration tests for updating the server name via the 1.1 api. --- nova/tests/api/openstack/test_servers.py | 18 +++++++++++++++++- nova/tests/integrated/api/client.py | 14 ++++++++++++++ nova/tests/integrated/test_servers.py | 19 +++++++++++++++++++ 3 files changed, 50 insertions(+), 1 deletion(-) diff --git a/nova/tests/api/openstack/test_servers.py b/nova/tests/api/openstack/test_servers.py index 775f66ad0..235b0a2ca 100644 --- a/nova/tests/api/openstack/test_servers.py +++ b/nova/tests/api/openstack/test_servers.py @@ -901,7 +901,7 @@ 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()) @@ -967,6 +967,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 +1000,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') diff --git a/nova/tests/integrated/api/client.py b/nova/tests/integrated/api/client.py index 59cc3b564..035a35aab 100644 --- a/nova/tests/integrated/api/client.py +++ b/nova/tests/integrated/api/client.py @@ -172,6 +172,17 @@ 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, 204]) @@ -187,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() -- cgit From 58eb29ebe4376a276a54f4fd984802a0e50fb8e3 Mon Sep 17 00:00:00 2001 From: Dan Prince Date: Fri, 15 Jul 2011 15:05:46 -0400 Subject: Updates to the XenServer agent plugin to fix file injection: -Update _agent_has_method so that it parses the features 'message' from nova-agent correctly. (it was trying to call .split on a dict). -Rip out the agent_has_method caching functionality which just plain isn't working with XenServer 5.6 SP2. -Pass the arg_dict to _agent_has_method. This fixes an issue where a subsequent call to xenstore.write_record didn't get the 'dom_id' (KeyError). -Fix a string formatting issue in inject_file in creating the b64 data. --- plugins/xenserver/xenapi/etc/xapi.d/plugins/agent | 44 ++++++++++------------- 1 file changed, 19 insertions(+), 25 deletions(-) diff --git a/plugins/xenserver/xenapi/etc/xapi.d/plugins/agent b/plugins/xenserver/xenapi/etc/xapi.d/plugins/agent index 68d7e7bff..b169a74bf 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 @@ -129,18 +129,18 @@ def inject_file(self, arg_dict): b64_path = arg_dict["b64_path"] b64_file = arg_dict["b64_contents"] request_id = arg_dict["id"] - if self._agent_has_method("file_inject"): + if _agent_has_method(self, "file_inject", arg_dict): # 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 _agent_has_method(self, "injectfile", arg_dict): # 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,24 @@ def agent_update(self, arg_dict): return resp -def _agent_has_method(self, method): +def _agent_has_method(self, method, arg_dict): """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. + features. """ + 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) + method_arr = response["message"].split(",") + # The agent returns a comma-separated list of methods in the message + return method in method_arr def _wait_for_agent(self, request_id, arg_dict): -- cgit From 3233d6cafb22305f09c1384ba30e677751cace6a Mon Sep 17 00:00:00 2001 From: Dan Prince Date: Sat, 16 Jul 2011 23:30:47 -0400 Subject: Change _agent_has_method to _get_agent_features. Update the inject files function so that it calls _get_agent_features only once per injected file. --- plugins/xenserver/xenapi/etc/xapi.d/plugins/agent | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/plugins/xenserver/xenapi/etc/xapi.d/plugins/agent b/plugins/xenserver/xenapi/etc/xapi.d/plugins/agent index b169a74bf..1321c820e 100755 --- a/plugins/xenserver/xenapi/etc/xapi.d/plugins/agent +++ b/plugins/xenserver/xenapi/etc/xapi.d/plugins/agent @@ -129,13 +129,14 @@ def inject_file(self, arg_dict): b64_path = arg_dict["b64_path"] b64_file = arg_dict["b64_contents"] request_id = arg_dict["id"] - if _agent_has_method(self, "file_inject", arg_dict): + 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 _agent_has_method(self, "injectfile", arg_dict): + 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) @@ -174,10 +175,8 @@ def agent_update(self, arg_dict): return resp -def _agent_has_method(self, method, arg_dict): - """Check that the agent has a particular method by checking its - features. - """ +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) @@ -189,9 +188,7 @@ def _agent_has_method(self, method, arg_dict): except TimeoutError, e: raise PluginError(e) response = json.loads(resp) - method_arr = response["message"].split(",") - # The agent returns a comma-separated list of methods in the message - return method in method_arr + return response["message"].split(",") def _wait_for_agent(self, request_id, arg_dict): -- cgit From b927674112849e7b4ebbd59c188d8f7a1eb47e2a Mon Sep 17 00:00:00 2001 From: Dan Prince Date: Mon, 18 Jul 2011 09:38:50 -0400 Subject: Check returncode in get_agent_features. --- plugins/xenserver/xenapi/etc/xapi.d/plugins/agent | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/plugins/xenserver/xenapi/etc/xapi.d/plugins/agent b/plugins/xenserver/xenapi/etc/xapi.d/plugins/agent index 1321c820e..41225e6f3 100755 --- a/plugins/xenserver/xenapi/etc/xapi.d/plugins/agent +++ b/plugins/xenserver/xenapi/etc/xapi.d/plugins/agent @@ -188,7 +188,10 @@ def _get_agent_features(self, arg_dict): except TimeoutError, e: raise PluginError(e) response = json.loads(resp) - return response["message"].split(",") + if response['returncode'] != '0': + return response["message"].split(",") + else: + return {} def _wait_for_agent(self, request_id, arg_dict): -- cgit From cf14d867673e944cc0c0d5fc160d2fbcfe56e98e Mon Sep 17 00:00:00 2001 From: Dan Prince Date: Mon, 18 Jul 2011 10:29:49 -0400 Subject: returncode is an integer. --- plugins/xenserver/xenapi/etc/xapi.d/plugins/agent | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/xenserver/xenapi/etc/xapi.d/plugins/agent b/plugins/xenserver/xenapi/etc/xapi.d/plugins/agent index 41225e6f3..8ec4fb061 100755 --- a/plugins/xenserver/xenapi/etc/xapi.d/plugins/agent +++ b/plugins/xenserver/xenapi/etc/xapi.d/plugins/agent @@ -188,7 +188,7 @@ def _get_agent_features(self, arg_dict): except TimeoutError, e: raise PluginError(e) response = json.loads(resp) - if response['returncode'] != '0': + if response['returncode'] != 0: return response["message"].split(",") else: return {} -- cgit From 813253e8bea9a8db9c1df45f9aa5e094503a015a Mon Sep 17 00:00:00 2001 From: Dan Prince Date: Mon, 18 Jul 2011 13:18:16 -0400 Subject: Don't jsonify the inject_file response. It is already json. --- plugins/xenserver/xenapi/etc/xapi.d/plugins/agent | 1 - 1 file changed, 1 deletion(-) diff --git a/plugins/xenserver/xenapi/etc/xapi.d/plugins/agent b/plugins/xenserver/xenapi/etc/xapi.d/plugins/agent index 8ec4fb061..288ccc78a 100755 --- a/plugins/xenserver/xenapi/etc/xapi.d/plugins/agent +++ b/plugins/xenserver/xenapi/etc/xapi.d/plugins/agent @@ -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 -- cgit