From 8c66d79a41044837a0865b1a706dd89e788597d1 Mon Sep 17 00:00:00 2001 From: Kei Masumoto Date: Thu, 14 Apr 2011 20:57:11 +0900 Subject: add kvm-pause and kvm-suspend --- nova/virt/libvirt_conn.py | 43 +++++++++++++++++++++++++++++++++++++++---- 1 file changed, 39 insertions(+), 4 deletions(-) (limited to 'nova') diff --git a/nova/virt/libvirt_conn.py b/nova/virt/libvirt_conn.py index 6ec15fbb8..66f43e786 100644 --- a/nova/virt/libvirt_conn.py +++ b/nova/virt/libvirt_conn.py @@ -545,19 +545,54 @@ class LibvirtConnection(driver.ComputeDriver): @exception.wrap_exception def pause(self, instance, callback): - raise exception.ApiError("pause not supported for libvirt.") + """Pause VM instance""" + if self.read_only: + tmpconn = self._connect(self.libvirt_uri, False) + dom = tmpconn.lookupByName(instance.name) + dom.suspend() + tmpconn.close() + else: + dom = self._conn.lookupByName(instance.name) + dom.suspend() @exception.wrap_exception def unpause(self, instance, callback): - raise exception.ApiError("unpause not supported for libvirt.") + """Unpause paused VM instance""" + if self.read_only: + tmpconn = self._connect(self.libvirt_uri, False) + dom = tmpconn.lookupByName(instance.name) + dom.resume() + tmpconn.close() + else: + dom = self._conn.lookupByName(instance.name) + dom.resume() @exception.wrap_exception def suspend(self, instance, callback): - raise exception.ApiError("suspend not supported for libvirt") + """Suspend the specified instance""" + if self.read_only: + tmpconn = self._connect(self.libvirt_uri, False) + dom = tmpconn.lookupByName(instance.name) + dom.managedSave(0) + tmpconn.close() + else: + dom = self._conn.lookupByName(instance.name) + dom.managedSave(0) @exception.wrap_exception def resume(self, instance, callback): - raise exception.ApiError("resume not supported for libvirt") + """resume the specified instance""" + try: + if self.read_only: + tmpconn = self._connect(self.libvirt_uri, False) + dom = tmpconn.lookupByName(instance.name) + tmpconn.close() + else: + dom = self._conn.lookupByName(instance.name) + dom.create() + except libvirt.LibvirtError: + xml = self.to_xml(instance, None) + self._create_new_domain(xml) @exception.wrap_exception def rescue(self, instance, callback=None): -- cgit From 0ba085928c75f2fc27fb03eaa3aaeff6618e8875 Mon Sep 17 00:00:00 2001 From: MORITA Kazutaka Date: Tue, 19 Apr 2011 20:48:26 +0900 Subject: Add support for creating a snapshot of a nova volume with euca-create-snapshot. --- nova/api/ec2/__init__.py | 6 ++ nova/api/ec2/cloud.py | 52 ++++++++++++--- nova/db/api.py | 39 +++++++++++ nova/db/sqlalchemy/api.py | 77 ++++++++++++++++++++++ .../versions/015_add_volume_snapshot_support.py | 71 ++++++++++++++++++++ nova/db/sqlalchemy/models.py | 24 +++++++ nova/exception.py | 50 ++++++++++++++ nova/volume/api.py | 44 +++++++++++++ nova/volume/driver.py | 8 +++ nova/volume/manager.py | 42 ++++++++++++ 10 files changed, 405 insertions(+), 8 deletions(-) create mode 100644 nova/db/sqlalchemy/migrate_repo/versions/015_add_volume_snapshot_support.py (limited to 'nova') diff --git a/nova/api/ec2/__init__.py b/nova/api/ec2/__init__.py index cd59340bd..4a49a5a6b 100644 --- a/nova/api/ec2/__init__.py +++ b/nova/api/ec2/__init__.py @@ -327,6 +327,12 @@ class Executor(wsgi.Application): ec2_id = ec2utils.id_to_ec2_id(ex.volume_id, 'vol-%08x') 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') + message = _('Snapshot %s not found') % ec2_id + return self._error(req, context, type(ex).__name__, message) except exception.NotFound as ex: LOG.info(_('NotFound raised: %s'), unicode(ex), context=context) return self._error(req, context, type(ex).__name__, unicode(ex)) diff --git a/nova/api/ec2/cloud.py b/nova/api/ec2/cloud.py index 092b80fa2..f5360af0b 100644 --- a/nova/api/ec2/cloud.py +++ b/nova/api/ec2/cloud.py @@ -280,14 +280,46 @@ class CloudController(object): owner=None, restorable_by=None, **kwargs): - return {'snapshotSet': [{'snapshotId': 'fixme', - 'volumeId': 'fixme', - 'status': 'fixme', - 'startTime': 'fixme', - 'progress': 'fixme', - 'ownerId': 'fixme', - 'volumeSize': 0, - 'description': 'fixme'}]} + if snapshot_id: + snapshots = [] + for ec2_id in snapshot_id: + internal_id = ec2utils.ec2_id_to_id(ec2_id) + snapshot = self.volume_api.get_snapshot(context, snapshot_id=internal_id) + snapshots.append(snapshot) + else: + snapshots = self.volume_api.get_all_snapshots(context) + snapshots = [self._format_snapshot(context, s) for s in snapshots] + return {'snapshotSet': snapshots} + + 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['status'] = snapshot['status'] + s['startTime'] = snapshot['created_at'] + s['progress'] = snapshot['progress'] + s['ownerId'] = snapshot['project_id'] + s['volumeSize'] = snapshot['volume_size'] + s['description'] = snapshot['display_description'] + + s['display_name'] = snapshot['display_name'] + s['display_description'] = snapshot['display_description'] + return s + + def create_snapshot(self, context, volume_id, **kwargs): + LOG.audit(_("Create snapshot of volume %s"), volume_id, context=context) + volume_id = ec2utils.ec2_id_to_id(volume_id) + snapshot = self.volume_api.create_snapshot( + context, + volume_id=volume_id, + name=kwargs.get('display_name'), + description=kwargs.get('display_description')) + return {'snapshotSet': [self._format_snapshot(context, snapshot)]} + + def delete_snapshot(self, context, snapshot_id, **kwargs): + snapshot_id = ec2utils.ec2_id_to_id(snapshot_id) + self.volume_api.delete_snapshot(context, snapshot_id=snapshot_id) + return True def describe_key_pairs(self, context, key_name=None, **kwargs): key_pairs = db.key_pair_get_all_by_user(context, context.user_id) @@ -595,6 +627,10 @@ class CloudController(object): 'volumeId': v['volumeId']}] else: v['attachmentSet'] = [{}] + if volume.get('snapshot_id') != None: + v['snapshotId'] = ec2utils.id_to_ec2_id(volume['snapshot_id'], 'snap-%08x') + else: + v['snapshotId'] = None v['display_name'] = volume['display_name'] v['display_description'] = volume['display_description'] diff --git a/nova/db/api.py b/nova/db/api.py index f9a4b5b4b..57e585a9c 100644 --- a/nova/db/api.py +++ b/nova/db/api.py @@ -47,6 +47,8 @@ flags.DEFINE_string('instance_name_template', 'instance-%08x', 'Template string to be used to generate instance names') flags.DEFINE_string('volume_name_template', 'volume-%08x', 'Template string to be used to generate instance names') +flags.DEFINE_string('snapshot_name_template', 'snapshot-%08x', + 'Template string to be used to generate instance names') IMPL = utils.LazyPluggable(FLAGS['db_backend'], @@ -871,6 +873,43 @@ def volume_update(context, volume_id, values): #################### +def snapshot_create(context, values): + """Create a volume from the values dictionary.""" + return IMPL.snapshot_create(context, values) + + +def snapshot_destroy(context, snapshot_id): + """Create a volume from the values dictionary.""" + return IMPL.snapshot_destroy(context, snapshot_id) + + +def snapshot_get(context, snapshot_id): + """Get a volume or raise if it does not exist.""" + return IMPL.snapshot_get(context, snapshot_id) + + +def snapshot_get_all(context): + """Get all volumes.""" + return IMPL.snapshot_get_all(context) + + +def snapshot_get_all_by_project(context, project_id): + """Get all volumes belonging to a project.""" + return IMPL.snapshot_get_all_by_project(context, project_id) + + +def snapshot_update(context, snapshot_id, values): + """Set the given properties on an snapshot and update it. + + Raises NotFound if snapshot does not exist. + + """ + return IMPL.snapshot_update(context, snapshot_id, values) + + +#################### + + def security_group_get_all(context): """Get all security groups.""" return IMPL.security_group_get_all(context) diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index 285b22a04..ebdb2ad5c 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -1758,6 +1758,83 @@ def volume_update(context, volume_id, values): ################### +@require_context +def snapshot_create(context, values): + snapshot_ref = models.Snapshot() + snapshot_ref.update(values) + + session = get_session() + with session.begin(): + snapshot_ref.save(session=session) + return snapshot_ref + + +@require_admin_context +def snapshot_destroy(context, snapshot_id): + session = get_session() + with session.begin(): + session.query(models.Snapshot).\ + filter_by(id=snapshot_id).\ + update({'deleted': 1, + 'deleted_at': datetime.datetime.utcnow(), + 'updated_at': literal_column('updated_at')}) + + +@require_context +def snapshot_get(context, snapshot_id, session=None): + if not session: + session = get_session() + result = None + + if is_admin_context(context): + result = session.query(models.Snapshot).\ + filter_by(id=snapshot_id).\ + filter_by(deleted=can_read_deleted(context)).\ + first() + elif is_user_context(context): + result = session.query(models.Snapshot).\ + filter_by(project_id=context.project_id).\ + filter_by(id=snapshot_id).\ + filter_by(deleted=False).\ + first() + if not result: + raise exception.SnapshotNotFound(_('Snapshot %s not found') % snapshot_id, + snapshot_id) + + return result + + +@require_admin_context +def snapshot_get_all(context): + session = get_session() + return session.query(models.Snapshot).\ + filter_by(deleted=can_read_deleted(context)).\ + all() + + +@require_context +def snapshot_get_all_by_project(context, project_id): + authorize_project_context(context, project_id) + + session = get_session() + return session.query(models.Snapshot).\ + filter_by(project_id=project_id).\ + filter_by(deleted=can_read_deleted(context)).\ + all() + + +@require_context +def snapshot_update(context, snapshot_id, values): + session = get_session() + with session.begin(): + snapshot_ref = snapshot_get(context, snapshot_id, session=session) + snapshot_ref.update(values) + snapshot_ref.save(session=session) + + +################### + + @require_context def security_group_get_all(context): session = get_session() diff --git a/nova/db/sqlalchemy/migrate_repo/versions/015_add_volume_snapshot_support.py b/nova/db/sqlalchemy/migrate_repo/versions/015_add_volume_snapshot_support.py new file mode 100644 index 000000000..288f63e72 --- /dev/null +++ b/nova/db/sqlalchemy/migrate_repo/versions/015_add_volume_snapshot_support.py @@ -0,0 +1,71 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 MORITA Kazutaka. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from sqlalchemy import * +from migrate import * + +from nova import log as logging + +meta = MetaData() + +snapshots = Table('snapshots', meta, + Column('created_at', DateTime(timezone=False)), + Column('updated_at', DateTime(timezone=False)), + Column('deleted_at', DateTime(timezone=False)), + Column('deleted', Boolean(create_constraint=True, name=None)), + Column('id', Integer(), primary_key=True, nullable=False), + Column('volume_id', Integer(), nullable=False), + Column('user_id', + String(length=255, convert_unicode=False, assert_unicode=None, + unicode_error=None, _warn_on_bytestring=False)), + Column('project_id', + String(length=255, convert_unicode=False, assert_unicode=None, + unicode_error=None, _warn_on_bytestring=False)), + Column('status', + String(length=255, convert_unicode=False, assert_unicode=None, + unicode_error=None, _warn_on_bytestring=False)), + Column('progress', + String(length=255, convert_unicode=False, assert_unicode=None, + unicode_error=None, _warn_on_bytestring=False)), + Column('volume_size', Integer()), + Column('scheduled_at', DateTime(timezone=False)), + Column('display_name', + String(length=255, convert_unicode=False, assert_unicode=None, + unicode_error=None, _warn_on_bytestring=False)), + Column('display_description', + String(length=255, convert_unicode=False, assert_unicode=None, + unicode_error=None, _warn_on_bytestring=False)) + ) + + +def upgrade(migrate_engine): + # Upgrade operations go here. Don't create your own engine; + # bind migrate_engine to your metadata + meta.bind = migrate_engine + + try: + snapshots.create() + except Exception: + logging.info(repr(snapshots)) + logging.exception('Exception while creating table') + meta.drop_all(tables=[snapshots]) + raise + + +def downgrade(migrate_engine): + # Operations to reverse the above upgrade go here. + snapshots.drop() diff --git a/nova/db/sqlalchemy/models.py b/nova/db/sqlalchemy/models.py index 36a084a1d..2e0ead5f9 100644 --- a/nova/db/sqlalchemy/models.py +++ b/nova/db/sqlalchemy/models.py @@ -327,6 +327,30 @@ class Quota(BASE, NovaBase): metadata_items = Column(Integer) +class Snapshot(BASE, NovaBase): + """Represents a block storage device that can be attached to a vm.""" + __tablename__ = 'snapshots' + id = Column(Integer, primary_key=True, autoincrement=True) + + @property + def name(self): + return FLAGS.snapshot_name_template % self.id + + @property + def volume_name(self): + return FLAGS.volume_name_template % self.volume_id + + user_id = Column(String(255)) + project_id = Column(String(255)) + + volume_id = Column(Integer) + status = Column(String(255)) + progress = Column(String(255)) + volume_size = Column(Integer) + + display_name = Column(String(255)) + display_description = Column(String(255)) + class ExportDevice(BASE, NovaBase): """Represates a shelf and blade that a volume can be exported on.""" __tablename__ = 'export_devices' diff --git a/nova/exception.py b/nova/exception.py index 9905fb19b..2dffeb795 100644 --- a/nova/exception.py +++ b/nova/exception.py @@ -60,6 +60,56 @@ class ApiError(Error): class BuildInProgress(Error): + super(ApiError, self).__init__('%s: %s' % (code, message)) + + +class NotFound(Error): + pass + + +class InstanceNotFound(NotFound): + def __init__(self, message, instance_id): + self.instance_id = instance_id + super(InstanceNotFound, self).__init__(message) + + +class VolumeNotFound(NotFound): + def __init__(self, message, volume_id): + self.volume_id = volume_id + super(VolumeNotFound, self).__init__(message) + + +class SnapshotNotFound(NotFound): + def __init__(self, message, snapshot_id): + self.snapshot_id = snapshot_id + super(SnapshotNotFound, self).__init__(message) + + +class Duplicate(Error): + pass + + +class NotAuthorized(Error): + pass + + +class NotEmpty(Error): + pass + + +class Invalid(Error): + pass + + +class InvalidInputException(Error): + pass + + +class InvalidContentType(Error): + pass + + +class TimeoutException(Error): pass diff --git a/nova/volume/api.py b/nova/volume/api.py index 09befb647..c1af30de0 100644 --- a/nova/volume/api.py +++ b/nova/volume/api.py @@ -90,6 +90,15 @@ class API(base.Base): return self.db.volume_get_all(context) return self.db.volume_get_all_by_project(context, context.project_id) + def get_snapshot(self, context, snapshot_id): + rv = self.db.snapshot_get(context, snapshot_id) + return dict(rv.iteritems()) + + def get_all_snapshots(self, context): + if context.is_admin: + return self.db.snapshot_get_all(context) + return self.db.snapshot_get_all_by_project(context, context.project_id) + def check_attach(self, context, volume_id): volume = self.get(context, volume_id) # TODO(vish): abstract status checking? @@ -110,3 +119,38 @@ class API(base.Base): self.db.queue_get_for(context, FLAGS.compute_topic, host), {"method": "remove_volume", "args": {'volume_id': volume_id}}) + + def create_snapshot(self, context, volume_id, name, description): + volume = self.get(context, volume_id) + if volume['status'] != "available": + raise exception.ApiError(_("Volume status must be available")) + + options = { + 'volume_id': volume_id, + 'user_id': context.user_id, + 'project_id': context.project_id, + 'status': "creating", + 'progress': '0%', + 'volume_size': volume['size'], + 'display_name': name, + 'display_description': description} + + snapshot = self.db.snapshot_create(context, options) + rpc.cast(context, + FLAGS.scheduler_topic, + {"method": "create_snapshot", + "args": {"topic": FLAGS.volume_topic, + "volume_id": volume_id, + "snapshot_id": snapshot['id']}}) + return snapshot + + def delete_snapshot(self, context, snapshot_id): + snapshot = self.get_snapshot(context, snapshot_id) + if snapshot['status'] != "available": + raise exception.ApiError(_("Snapshot status must be available")) + self.db.snapshot_update(context, snapshot_id, {'status': 'deleting'}) + rpc.cast(context, + FLAGS.scheduler_topic, + {"method": "delete_snapshot", + "args": {"topic": FLAGS.volume_topic, + "snapshot_id": snapshot_id}}) diff --git a/nova/volume/driver.py b/nova/volume/driver.py index 55307ad9b..31998e307 100644 --- a/nova/volume/driver.py +++ b/nova/volume/driver.py @@ -122,6 +122,14 @@ class VolumeDriver(object): (FLAGS.volume_group, volume['name'])) + def create_snapshot(self, snapshot): + """Creates a snapshot.""" + raise NotImplementedError() + + def delete_snapshot(self, snapshot): + """Deletes a snapshot.""" + raise NotImplementedError() + def local_path(self, volume): # NOTE(vish): stops deprecation warning escaped_group = FLAGS.volume_group.replace('-', '--') diff --git a/nova/volume/manager.py b/nova/volume/manager.py index 2178389ce..87fd3bf17 100644 --- a/nova/volume/manager.py +++ b/nova/volume/manager.py @@ -152,6 +152,48 @@ class VolumeManager(manager.SchedulerDependentManager): LOG.debug(_("volume %s: deleted successfully"), volume_ref['name']) return True + def create_snapshot(self, context, volume_id, snapshot_id): + """Creates and exports the snapshot.""" + context = context.elevated() + snapshot_ref = self.db.snapshot_get(context, snapshot_id) + LOG.info(_("snapshot %s: creating"), snapshot_ref['name']) + + try: + snap_name = snapshot_ref['name'] + LOG.debug(_("snapshot %(snap_name)s: creating") % locals()) + model_update = self.driver.create_snapshot(snapshot_ref) + if model_update: + self.db.snapshot_update(context, snapshot_ref['id'], model_update) + + except Exception: + self.db.snapshot_update(context, + snapshot_ref['id'], {'status': 'error'}) + raise + + self.db.snapshot_update(context, + snapshot_ref['id'], {'status': 'available', + 'progress': '100%'}) + LOG.debug(_("snapshot %s: created successfully"), snapshot_ref['name']) + return snapshot_id + + def delete_snapshot(self, context, snapshot_id): + """Deletes and unexports snapshot.""" + context = context.elevated() + snapshot_ref = self.db.snapshot_get(context, snapshot_id) + + try: + LOG.debug(_("snapshot %s: deleting"), snapshot_ref['name']) + self.driver.delete_snapshot(snapshot_ref) + except Exception: + self.db.snapshot_update(context, + snapshot_ref['id'], + {'status': 'error_deleting'}) + raise + + self.db.snapshot_destroy(context, snapshot_id) + LOG.debug(_("snapshot %s: deleted successfully"), snapshot_ref['name']) + return True + def setup_compute_volume(self, context, volume_id): """Setup remote volume on compute host. -- cgit From f76f2ee50f2407155a0aaefac3224e6af14e7d26 Mon Sep 17 00:00:00 2001 From: MORITA Kazutaka Date: Tue, 19 Apr 2011 20:50:10 +0900 Subject: Add support for creating a Sheepdog snapshot. --- nova/volume/driver.py | 10 ++++++++++ 1 file changed, 10 insertions(+) (limited to 'nova') diff --git a/nova/volume/driver.py b/nova/volume/driver.py index 31998e307..ba0a7efef 100644 --- a/nova/volume/driver.py +++ b/nova/volume/driver.py @@ -620,6 +620,16 @@ class SheepdogDriver(VolumeDriver): """Deletes a logical volume""" self._try_execute('collie', 'vdi', 'delete', volume['name']) + def create_snapshot(self, snapshot): + """Creates a sheepdog snapshot""" + self._try_execute('qemu-img', 'snapshot', '-c', snapshot['name'], + "sheepdog:%s" % snapshot['volume_name']) + + def delete_snapshot(self, snapshot): + """Deletes a sheepdog snapshot""" + self._try_execute('collie', 'vdi', 'delete', snapshot['volume_name'], + '-s', snapshot['name']) + def local_path(self, volume): return "sheepdog:%s" % volume['name'] -- cgit From 2f3819628b6d3dea13a56ea6e93e02992b2e1f5f Mon Sep 17 00:00:00 2001 From: MORITA Kazutaka Date: Tue, 19 Apr 2011 21:01:25 +0900 Subject: Add support for creating a new volume from a existing snapshot with EC2 API. --- nova/api/ec2/cloud.py | 12 +++++- .../versions/016_add_snapshot_id_to_volumes.py | 48 ++++++++++++++++++++++ nova/db/sqlalchemy/models.py | 2 + nova/volume/api.py | 12 +++++- nova/volume/driver.py | 4 ++ nova/volume/manager.py | 9 +++- 6 files changed, 81 insertions(+), 6 deletions(-) create mode 100644 nova/db/sqlalchemy/migrate_repo/versions/016_add_snapshot_id_to_volumes.py (limited to 'nova') diff --git a/nova/api/ec2/cloud.py b/nova/api/ec2/cloud.py index f5360af0b..aa15539ac 100644 --- a/nova/api/ec2/cloud.py +++ b/nova/api/ec2/cloud.py @@ -636,11 +636,19 @@ class CloudController(object): v['display_description'] = volume['display_description'] return v - def create_volume(self, context, size, **kwargs): - LOG.audit(_("Create volume of %s GB"), size, context=context) + def create_volume(self, context, **kwargs): + size = kwargs.get('size'); + if kwargs.get('snapshot_id') != None: + snapshot_id = ec2utils.ec2_id_to_id(kwargs['snapshot_id']) + LOG.audit(_("Create volume from snapshot %s"), snapshot_id, context=context) + else: + snapshot_id = None + LOG.audit(_("Create volume of %s GB"), size, context=context) + volume = self.volume_api.create( context, size=size, + snapshot_id=snapshot_id, name=kwargs.get('display_name'), description=kwargs.get('display_description')) # TODO(vish): Instance should be None at db layer instead of diff --git a/nova/db/sqlalchemy/migrate_repo/versions/016_add_snapshot_id_to_volumes.py b/nova/db/sqlalchemy/migrate_repo/versions/016_add_snapshot_id_to_volumes.py new file mode 100644 index 000000000..0a50123bf --- /dev/null +++ b/nova/db/sqlalchemy/migrate_repo/versions/016_add_snapshot_id_to_volumes.py @@ -0,0 +1,48 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 MORITA Kazutaka. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from sqlalchemy import * +from migrate import * + +from nova import log as logging + + +meta = MetaData() + + +# Table stub-definitions +# Just for the ForeignKey and column creation to succeed, these are not the +# actual definitions of instances or services. +# +volumes = Table('volumes', meta, + Column('id', Integer(), primary_key=True, nullable=False), + ) + +# +# New Column +# + +snapshot_id = Column('snapshot_id', Integer()) + + +def upgrade(migrate_engine): + # Upgrade operations go here. Don't create your own engine; + # bind migrate_engine to your metadata + meta.bind = migrate_engine + + # Add columns to existing tables + volumes.create_column(snapshot_id) diff --git a/nova/db/sqlalchemy/models.py b/nova/db/sqlalchemy/models.py index 2e0ead5f9..ca762ca9f 100644 --- a/nova/db/sqlalchemy/models.py +++ b/nova/db/sqlalchemy/models.py @@ -287,6 +287,8 @@ class Volume(BASE, NovaBase): user_id = Column(String(255)) project_id = Column(String(255)) + snapshot_id = Column(String(255)) + host = Column(String(255)) # , ForeignKey('hosts.id')) size = Column(Integer) availability_zone = Column(String(255)) # TODO(vish): foreign key? diff --git a/nova/volume/api.py b/nova/volume/api.py index c1af30de0..7fa80383b 100644 --- a/nova/volume/api.py +++ b/nova/volume/api.py @@ -39,7 +39,13 @@ LOG = logging.getLogger('nova.volume') class API(base.Base): """API for interacting with the volume manager.""" - def create(self, context, size, name, description): + def create(self, context, size, snapshot_id, name, description): + if snapshot_id != None: + snapshot = self.get_snapshot(context, snapshot_id) + if snapshot['status'] != "available": + raise exception.ApiError(_("Snapshot status must be available")) + size = snapshot['volume_size'] + if quota.allowed_volumes(context, 1, size) < 1: pid = context.project_id LOG.warn(_("Quota exceeeded for %(pid)s, tried to create" @@ -51,6 +57,7 @@ class API(base.Base): 'size': size, 'user_id': context.user_id, 'project_id': context.project_id, + 'snapshot_id': snapshot_id, 'availability_zone': FLAGS.storage_availability_zone, 'status': "creating", 'attach_status': "detached", @@ -62,7 +69,8 @@ class API(base.Base): FLAGS.scheduler_topic, {"method": "create_volume", "args": {"topic": FLAGS.volume_topic, - "volume_id": volume['id']}}) + "volume_id": volume['id'], + "snapshot_id": snapshot_id}}) return volume def delete(self, context, volume_id): diff --git a/nova/volume/driver.py b/nova/volume/driver.py index ba0a7efef..02b0d50f4 100644 --- a/nova/volume/driver.py +++ b/nova/volume/driver.py @@ -101,6 +101,10 @@ class VolumeDriver(object): volume['name'], FLAGS.volume_group) + def create_volume_from_snapshot(self, volume, snapshot): + """Creates a volume from a snapshot.""" + raise NotImplementedError() + def delete_volume(self, volume): """Deletes a logical volume.""" try: diff --git a/nova/volume/manager.py b/nova/volume/manager.py index 87fd3bf17..7d47fc191 100644 --- a/nova/volume/manager.py +++ b/nova/volume/manager.py @@ -90,7 +90,7 @@ class VolumeManager(manager.SchedulerDependentManager): else: LOG.info(_("volume %s: skipping export"), volume['name']) - def create_volume(self, context, volume_id): + def create_volume(self, context, volume_id, snapshot_id): """Creates and exports the volume.""" context = context.elevated() volume_ref = self.db.volume_get(context, volume_id) @@ -108,7 +108,12 @@ class VolumeManager(manager.SchedulerDependentManager): vol_size = volume_ref['size'] LOG.debug(_("volume %(vol_name)s: creating lv of" " size %(vol_size)sG") % locals()) - model_update = self.driver.create_volume(volume_ref) + if snapshot_id == None: + model_update = self.driver.create_volume(volume_ref) + else: + snapshot_ref = self.db.snapshot_get(context, snapshot_id) + model_update = self.driver.create_volume_from_snapshot(volume_ref, + snapshot_ref) if model_update: self.db.volume_update(context, volume_ref['id'], model_update) -- cgit From 1018a60e3194e7e283cd89af28efd689623058a8 Mon Sep 17 00:00:00 2001 From: MORITA Kazutaka Date: Tue, 19 Apr 2011 21:01:25 +0900 Subject: Add support for creating a new volume from a existing snapshot with EC2 API. --- nova/api/ec2/cloud.py | 12 +++++- .../versions/016_add_snapshot_id_to_volumes.py | 48 ++++++++++++++++++++++ nova/db/sqlalchemy/models.py | 2 + nova/volume/api.py | 12 +++++- nova/volume/driver.py | 4 ++ nova/volume/manager.py | 9 +++- 6 files changed, 81 insertions(+), 6 deletions(-) create mode 100644 nova/db/sqlalchemy/migrate_repo/versions/016_add_snapshot_id_to_volumes.py (limited to 'nova') diff --git a/nova/api/ec2/cloud.py b/nova/api/ec2/cloud.py index 6daf299b9..5d4d2ad27 100644 --- a/nova/api/ec2/cloud.py +++ b/nova/api/ec2/cloud.py @@ -639,11 +639,19 @@ class CloudController(object): v['display_description'] = volume['display_description'] return v - def create_volume(self, context, size, **kwargs): - LOG.audit(_("Create volume of %s GB"), size, context=context) + def create_volume(self, context, **kwargs): + size = kwargs.get('size'); + if kwargs.get('snapshot_id') != None: + snapshot_id = ec2utils.ec2_id_to_id(kwargs['snapshot_id']) + LOG.audit(_("Create volume from snapshot %s"), snapshot_id, context=context) + else: + snapshot_id = None + LOG.audit(_("Create volume of %s GB"), size, context=context) + volume = self.volume_api.create( context, size=size, + snapshot_id=snapshot_id, name=kwargs.get('display_name'), description=kwargs.get('display_description')) # TODO(vish): Instance should be None at db layer instead of diff --git a/nova/db/sqlalchemy/migrate_repo/versions/016_add_snapshot_id_to_volumes.py b/nova/db/sqlalchemy/migrate_repo/versions/016_add_snapshot_id_to_volumes.py new file mode 100644 index 000000000..0a50123bf --- /dev/null +++ b/nova/db/sqlalchemy/migrate_repo/versions/016_add_snapshot_id_to_volumes.py @@ -0,0 +1,48 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 MORITA Kazutaka. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from sqlalchemy import * +from migrate import * + +from nova import log as logging + + +meta = MetaData() + + +# Table stub-definitions +# Just for the ForeignKey and column creation to succeed, these are not the +# actual definitions of instances or services. +# +volumes = Table('volumes', meta, + Column('id', Integer(), primary_key=True, nullable=False), + ) + +# +# New Column +# + +snapshot_id = Column('snapshot_id', Integer()) + + +def upgrade(migrate_engine): + # Upgrade operations go here. Don't create your own engine; + # bind migrate_engine to your metadata + meta.bind = migrate_engine + + # Add columns to existing tables + volumes.create_column(snapshot_id) diff --git a/nova/db/sqlalchemy/models.py b/nova/db/sqlalchemy/models.py index 9abe4d9ae..afc2ea4e4 100644 --- a/nova/db/sqlalchemy/models.py +++ b/nova/db/sqlalchemy/models.py @@ -287,6 +287,8 @@ class Volume(BASE, NovaBase): user_id = Column(String(255)) project_id = Column(String(255)) + snapshot_id = Column(String(255)) + host = Column(String(255)) # , ForeignKey('hosts.id')) size = Column(Integer) availability_zone = Column(String(255)) # TODO(vish): foreign key? diff --git a/nova/volume/api.py b/nova/volume/api.py index f5285f31f..bd073964d 100644 --- a/nova/volume/api.py +++ b/nova/volume/api.py @@ -39,7 +39,13 @@ LOG = logging.getLogger('nova.volume') class API(base.Base): """API for interacting with the volume manager.""" - def create(self, context, size, name, description): + def create(self, context, size, snapshot_id, name, description): + if snapshot_id != None: + snapshot = self.get_snapshot(context, snapshot_id) + if snapshot['status'] != "available": + raise exception.ApiError(_("Snapshot status must be available")) + size = snapshot['volume_size'] + if quota.allowed_volumes(context, 1, size) < 1: pid = context.project_id LOG.warn(_("Quota exceeeded for %(pid)s, tried to create" @@ -51,6 +57,7 @@ class API(base.Base): 'size': size, 'user_id': context.user_id, 'project_id': context.project_id, + 'snapshot_id': snapshot_id, 'availability_zone': FLAGS.storage_availability_zone, 'status': "creating", 'attach_status': "detached", @@ -62,7 +69,8 @@ class API(base.Base): FLAGS.scheduler_topic, {"method": "create_volume", "args": {"topic": FLAGS.volume_topic, - "volume_id": volume['id']}}) + "volume_id": volume['id'], + "snapshot_id": snapshot_id}}) return volume def delete(self, context, volume_id): diff --git a/nova/volume/driver.py b/nova/volume/driver.py index ba0a7efef..02b0d50f4 100644 --- a/nova/volume/driver.py +++ b/nova/volume/driver.py @@ -101,6 +101,10 @@ class VolumeDriver(object): volume['name'], FLAGS.volume_group) + def create_volume_from_snapshot(self, volume, snapshot): + """Creates a volume from a snapshot.""" + raise NotImplementedError() + def delete_volume(self, volume): """Deletes a logical volume.""" try: diff --git a/nova/volume/manager.py b/nova/volume/manager.py index 87fd3bf17..7d47fc191 100644 --- a/nova/volume/manager.py +++ b/nova/volume/manager.py @@ -90,7 +90,7 @@ class VolumeManager(manager.SchedulerDependentManager): else: LOG.info(_("volume %s: skipping export"), volume['name']) - def create_volume(self, context, volume_id): + def create_volume(self, context, volume_id, snapshot_id): """Creates and exports the volume.""" context = context.elevated() volume_ref = self.db.volume_get(context, volume_id) @@ -108,7 +108,12 @@ class VolumeManager(manager.SchedulerDependentManager): vol_size = volume_ref['size'] LOG.debug(_("volume %(vol_name)s: creating lv of" " size %(vol_size)sG") % locals()) - model_update = self.driver.create_volume(volume_ref) + if snapshot_id == None: + model_update = self.driver.create_volume(volume_ref) + else: + snapshot_ref = self.db.snapshot_get(context, snapshot_id) + model_update = self.driver.create_volume_from_snapshot(volume_ref, + snapshot_ref) if model_update: self.db.volume_update(context, volume_ref['id'], model_update) -- cgit From 1c7c53a9f40a88eb9def7ab9d706e7399ad5e65b Mon Sep 17 00:00:00 2001 From: MORITA Kazutaka Date: Tue, 19 Apr 2011 21:02:00 +0900 Subject: Add support for cloning a Sheepdog volume. --- nova/volume/driver.py | 7 +++++++ 1 file changed, 7 insertions(+) (limited to 'nova') diff --git a/nova/volume/driver.py b/nova/volume/driver.py index 02b0d50f4..3f3caf37a 100644 --- a/nova/volume/driver.py +++ b/nova/volume/driver.py @@ -620,6 +620,13 @@ class SheepdogDriver(VolumeDriver): "sheepdog:%s" % volume['name'], sizestr) + def create_volume_from_snapshot(self, volume, snapshot): + """Creates a sheepdog volume from a snapshot.""" + self._try_execute('qemu-img', 'create', '-b', + "sheepdog:%s:%s" % (snapshot['volume_name'], snapshot['name']), + "sheepdog:%s" % volume['name']) + + def delete_volume(self, volume): """Deletes a logical volume""" self._try_execute('collie', 'vdi', 'delete', volume['name']) -- cgit From 5b670fe9bca9103642967bce609853704d0d1b88 Mon Sep 17 00:00:00 2001 From: MORITA Kazutaka Date: Tue, 19 Apr 2011 21:02:00 +0900 Subject: Add support for cloning a Sheepdog volume. --- nova/volume/driver.py | 7 +++++++ 1 file changed, 7 insertions(+) (limited to 'nova') diff --git a/nova/volume/driver.py b/nova/volume/driver.py index 02b0d50f4..3f3caf37a 100644 --- a/nova/volume/driver.py +++ b/nova/volume/driver.py @@ -620,6 +620,13 @@ class SheepdogDriver(VolumeDriver): "sheepdog:%s" % volume['name'], sizestr) + def create_volume_from_snapshot(self, volume, snapshot): + """Creates a sheepdog volume from a snapshot.""" + self._try_execute('qemu-img', 'create', '-b', + "sheepdog:%s:%s" % (snapshot['volume_name'], snapshot['name']), + "sheepdog:%s" % volume['name']) + + def delete_volume(self, volume): """Deletes a logical volume""" self._try_execute('collie', 'vdi', 'delete', volume['name']) -- cgit From 389f7c79199d5ad908a72375a7377a1122f36707 Mon Sep 17 00:00:00 2001 From: Isaku Yamahata Date: Mon, 9 May 2011 17:52:26 +0900 Subject: volume/driver: factor out lvm opration Factor out lvm operation for implementing basic snapshot later. --- nova/volume/driver.py | 62 ++++++++++++++++++++++++++++----------------------- 1 file changed, 34 insertions(+), 28 deletions(-) (limited to 'nova') diff --git a/nova/volume/driver.py b/nova/volume/driver.py index 3f3caf37a..9591c93d0 100644 --- a/nova/volume/driver.py +++ b/nova/volume/driver.py @@ -90,16 +90,40 @@ class VolumeDriver(object): raise exception.Error(_("volume group %s doesn't exist") % FLAGS.volume_group) + def _create_volume(self, volume_name, sizestr): + self._try_execute('sudo', 'lvcreate', '-L', sizestr, '-n', + volume_name, FLAGS.volume_group) + + def _copy_volume(self, srcstr, deststr, size_in_g): + self._execute('sudo', 'dd', 'if=%s' % srcstr, 'of=%s' % deststr, + 'count=%d' % (size_in_g * 1024), 'bs=1M') + + def _volume_not_present(self, volume_name): + path_name = '%s/%s' % (FLAGS.volume_group, volume_name) + try: + self._try_execute('sudo', 'lvdisplay', path_name) + except Exception as e: + # If the volume isn't present + return True + return False + + def _delete_volume(self, volume, size_in_g): + """Deletes a logical volume.""" + # zero out old volumes to prevent data leaking between users + # TODO(ja): reclaiming space should be done lazy and low priority + self._copy_volume('/dev/zero', self.local_path(volume), size_in_g) + self._try_execute('sudo', 'lvremove', '-f', "%s/%s" % + (FLAGS.volume_group, volume['name'])) + + def _sizestr(self, size_in_g): + if int(size_in_g) == 0: + return '100M' + return '%sG' % size_in_g + def create_volume(self, volume): """Creates a logical volume. Can optionally return a Dictionary of changes to the volume object to be persisted.""" - if int(volume['size']) == 0: - sizestr = '100M' - else: - sizestr = '%sG' % volume['size'] - self._try_execute('sudo', 'lvcreate', '-L', sizestr, '-n', - volume['name'], - FLAGS.volume_group) + self._create_volume(volume['name'], self._sizestr(volume['size'])) def create_volume_from_snapshot(self, volume, snapshot): """Creates a volume from a snapshot.""" @@ -107,24 +131,10 @@ class VolumeDriver(object): def delete_volume(self, volume): """Deletes a logical volume.""" - try: - self._try_execute('sudo', 'lvdisplay', - '%s/%s' % - (FLAGS.volume_group, - volume['name'])) - except Exception as e: + if self._volume_not_present(volume['name']): # If the volume isn't present, then don't attempt to delete return True - - # zero out old volumes to prevent data leaking between users - # TODO(ja): reclaiming space should be done lazy and low priority - self._execute('sudo', 'dd', 'if=/dev/zero', - 'of=%s' % self.local_path(volume), - 'count=%d' % (volume['size'] * 1024), - 'bs=1M') - self._try_execute('sudo', 'lvremove', '-f', "%s/%s" % - (FLAGS.volume_group, - volume['name'])) + self._delete_volume(volume, volume['size']) def create_snapshot(self, snapshot): """Creates a snapshot.""" @@ -612,13 +622,9 @@ class SheepdogDriver(VolumeDriver): def create_volume(self, volume): """Creates a sheepdog volume""" - if int(volume['size']) == 0: - sizestr = '100M' - else: - sizestr = '%sG' % volume['size'] self._try_execute('qemu-img', 'create', "sheepdog:%s" % volume['name'], - sizestr) + self._sizestr(volume['size'])) def create_volume_from_snapshot(self, volume, snapshot): """Creates a sheepdog volume from a snapshot.""" -- cgit From 03c735bb186a44d80a9d595e00e9c06fd8f709cc Mon Sep 17 00:00:00 2001 From: Isaku Yamahata Date: Mon, 9 May 2011 17:53:25 +0900 Subject: volume/driver: implement basic snapshot/clone added basic support for snapshot/clone to VolumeDriver. The implementation is not effective, but works. The effective implementation should be done by drived driver class. --- nova/exception.py | 6 ++++++ nova/volume/driver.py | 42 +++++++++++++++++++++++++++++++++++++----- nova/volume/manager.py | 6 ++++++ 3 files changed, 49 insertions(+), 5 deletions(-) (limited to 'nova') diff --git a/nova/exception.py b/nova/exception.py index 2dffeb795..6748ef265 100644 --- a/nova/exception.py +++ b/nova/exception.py @@ -79,6 +79,12 @@ class VolumeNotFound(NotFound): super(VolumeNotFound, self).__init__(message) +class VolumeIsBusy(Error): + def __init__(self, message, volume_id): + self.volume_id = volume_id + super(Error, self).__init__(message) + + class SnapshotNotFound(NotFound): def __init__(self, message, snapshot_id): self.snapshot_id = snapshot_id diff --git a/nova/volume/driver.py b/nova/volume/driver.py index 9591c93d0..457a1c9e6 100644 --- a/nova/volume/driver.py +++ b/nova/volume/driver.py @@ -113,13 +113,21 @@ class VolumeDriver(object): # TODO(ja): reclaiming space should be done lazy and low priority self._copy_volume('/dev/zero', self.local_path(volume), size_in_g) self._try_execute('sudo', 'lvremove', '-f', "%s/%s" % - (FLAGS.volume_group, volume['name'])) + (FLAGS.volume_group, + self._escape_snapshot(volume['name']))) def _sizestr(self, size_in_g): if int(size_in_g) == 0: return '100M' return '%sG' % size_in_g + # Linux LVM reserves name that starts with snapshot, so that + # such volume name can't be created. Mangle it. + def _escape_snapshot(self, snapshot_name): + if not snapshot_name.startswith('snapshot'): + return snapshot_name + return '_' + snapshot_name + def create_volume(self, volume): """Creates a logical volume. Can optionally return a Dictionary of changes to the volume object to be persisted.""" @@ -127,27 +135,51 @@ class VolumeDriver(object): def create_volume_from_snapshot(self, volume, snapshot): """Creates a volume from a snapshot.""" - raise NotImplementedError() + self._create_volume(volume['name'], self._sizestr(volume['size'])) + self._copy_volume(self.local_path(snapshot), self.local_path(volume), + snapshot['volume_size']) def delete_volume(self, volume): """Deletes a logical volume.""" if self._volume_not_present(volume['name']): # If the volume isn't present, then don't attempt to delete return True + + # TODO(yamahata): lvm can't delete origin volume only without + # deleting derived snapshots. Can we do something fancy? + out, err = self._execute('sudo', 'lvdisplay', '--noheading', + '-C', '-o', 'Attr', + '%s/%s' % (FLAGS.volume_group, + volume['name'])) + out = out.strip() + if (out[0] == 'o') or (out[0] == 'O'): + raise exception.VolumeIsBusy( + _('deleting volume %s that has snapshot'), volume['name']) + self._delete_volume(volume, volume['size']) def create_snapshot(self, snapshot): """Creates a snapshot.""" - raise NotImplementedError() + orig_lv_name = "%s/%s" % (FLAGS.volume_group, snapshot['volume_name']) + self._try_execute('sudo', 'lvcreate', '-L', + self._sizestr(snapshot['volume_size']), + '--name', self._escape_snapshot(snapshot['name']), + '--snapshot', orig_lv_name) def delete_snapshot(self, snapshot): """Deletes a snapshot.""" - raise NotImplementedError() + if self._volume_not_present(self._escape_snapshot(snapshot['name'])): + # If the snapshot isn't present, then don't attempt to delete + return True + + # TODO(yamahata): zeroing out the whole snapshot triggers COW. + # it's quite slow. + self._delete_volume(snapshot, snapshot['volume_size']) def local_path(self, volume): # NOTE(vish): stops deprecation warning escaped_group = FLAGS.volume_group.replace('-', '--') - escaped_name = volume['name'].replace('-', '--') + escaped_name = self._escape_snapshot(volume['name']).replace('-', '--') return "/dev/mapper/%s-%s" % (escaped_group, escaped_name) def ensure_export(self, context, volume): diff --git a/nova/volume/manager.py b/nova/volume/manager.py index 7d47fc191..84085fbd8 100644 --- a/nova/volume/manager.py +++ b/nova/volume/manager.py @@ -147,6 +147,12 @@ class VolumeManager(manager.SchedulerDependentManager): self.driver.remove_export(context, volume_ref) LOG.debug(_("volume %s: deleting"), volume_ref['name']) self.driver.delete_volume(volume_ref) + except exception.VolumeIsBusy, e: + LOG.debug(_("volume %s: volume is busy"), volume_ref['name']) + self.driver.ensure_export(context, volume_ref) + self.db.volume_update(context, volume_ref['id'], + {'status': 'available'}) + return True except Exception: self.db.volume_update(context, volume_ref['id'], -- cgit From db148f108dfc4829e1302a54fe4f57ab81212786 Mon Sep 17 00:00:00 2001 From: Isaku Yamahata Date: Mon, 9 May 2011 19:25:02 +0900 Subject: fix mismerge by 1059 --- nova/db/sqlalchemy/api.py | 3 +-- nova/exception.py | 65 ++++++----------------------------------------- nova/volume/driver.py | 3 +-- 3 files changed, 10 insertions(+), 61 deletions(-) (limited to 'nova') diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index ebdb2ad5c..7302f25b0 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -1798,8 +1798,7 @@ def snapshot_get(context, snapshot_id, session=None): filter_by(deleted=False).\ first() if not result: - raise exception.SnapshotNotFound(_('Snapshot %s not found') % snapshot_id, - snapshot_id) + raise exception.SnapshotNotFound(snapshot_id=snapshot_id) return result diff --git a/nova/exception.py b/nova/exception.py index 6748ef265..b16ea6810 100644 --- a/nova/exception.py +++ b/nova/exception.py @@ -60,65 +60,8 @@ class ApiError(Error): class BuildInProgress(Error): - super(ApiError, self).__init__('%s: %s' % (code, message)) - - -class NotFound(Error): - pass - - -class InstanceNotFound(NotFound): - def __init__(self, message, instance_id): - self.instance_id = instance_id - super(InstanceNotFound, self).__init__(message) - - -class VolumeNotFound(NotFound): - def __init__(self, message, volume_id): - self.volume_id = volume_id - super(VolumeNotFound, self).__init__(message) - - -class VolumeIsBusy(Error): - def __init__(self, message, volume_id): - self.volume_id = volume_id - super(Error, self).__init__(message) - - -class SnapshotNotFound(NotFound): - def __init__(self, message, snapshot_id): - self.snapshot_id = snapshot_id - super(SnapshotNotFound, self).__init__(message) - - -class Duplicate(Error): pass - -class NotAuthorized(Error): - pass - - -class NotEmpty(Error): - pass - - -class Invalid(Error): - pass - - -class InvalidInputException(Error): - pass - - -class InvalidContentType(Error): - pass - - -class TimeoutException(Error): - pass - - class DBError(Error): """Wraps an implementation specific exception.""" def __init__(self, inner_exception): @@ -319,6 +262,14 @@ class VolumeNotFoundForInstance(VolumeNotFound): message = _("Volume not found for instance %(instance_id)s.") +class SnapshotNotFound(NotFound): + message = _("Snapshot %(snapshot_id)s not found") + + +class VolumeIsBusy(Error): + message = _("deleting volume %(volume_name)s that has snapshot") + + class ExportDeviceNotFoundForVolume(NotFound): message = _("No export device found for volume %(volume_id)s.") diff --git a/nova/volume/driver.py b/nova/volume/driver.py index 457a1c9e6..e783d3a5a 100644 --- a/nova/volume/driver.py +++ b/nova/volume/driver.py @@ -153,8 +153,7 @@ class VolumeDriver(object): volume['name'])) out = out.strip() if (out[0] == 'o') or (out[0] == 'O'): - raise exception.VolumeIsBusy( - _('deleting volume %s that has snapshot'), volume['name']) + raise exception.VolumeIsBusy(volume_name=volume['name']) self._delete_volume(volume, volume['size']) -- cgit From c5dbee818b1a06bf5358c32197c8e15ecf0f660d Mon Sep 17 00:00:00 2001 From: Isaku Yamahata Date: Mon, 9 May 2011 20:19:35 +0900 Subject: db: fix db versioning --- .../versions/015_add_volume_snapshot_support.py | 71 ---------------------- .../versions/016_add_snapshot_id_to_volumes.py | 48 --------------- .../versions/016_add_volume_snapshot_support.py | 71 ++++++++++++++++++++++ .../versions/017_add_snapshot_id_to_volumes.py | 48 +++++++++++++++ 4 files changed, 119 insertions(+), 119 deletions(-) delete mode 100644 nova/db/sqlalchemy/migrate_repo/versions/015_add_volume_snapshot_support.py delete mode 100644 nova/db/sqlalchemy/migrate_repo/versions/016_add_snapshot_id_to_volumes.py create mode 100644 nova/db/sqlalchemy/migrate_repo/versions/016_add_volume_snapshot_support.py create mode 100644 nova/db/sqlalchemy/migrate_repo/versions/017_add_snapshot_id_to_volumes.py (limited to 'nova') diff --git a/nova/db/sqlalchemy/migrate_repo/versions/015_add_volume_snapshot_support.py b/nova/db/sqlalchemy/migrate_repo/versions/015_add_volume_snapshot_support.py deleted file mode 100644 index 288f63e72..000000000 --- a/nova/db/sqlalchemy/migrate_repo/versions/015_add_volume_snapshot_support.py +++ /dev/null @@ -1,71 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 MORITA Kazutaka. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -from sqlalchemy import * -from migrate import * - -from nova import log as logging - -meta = MetaData() - -snapshots = Table('snapshots', meta, - Column('created_at', DateTime(timezone=False)), - Column('updated_at', DateTime(timezone=False)), - Column('deleted_at', DateTime(timezone=False)), - Column('deleted', Boolean(create_constraint=True, name=None)), - Column('id', Integer(), primary_key=True, nullable=False), - Column('volume_id', Integer(), nullable=False), - Column('user_id', - String(length=255, convert_unicode=False, assert_unicode=None, - unicode_error=None, _warn_on_bytestring=False)), - Column('project_id', - String(length=255, convert_unicode=False, assert_unicode=None, - unicode_error=None, _warn_on_bytestring=False)), - Column('status', - String(length=255, convert_unicode=False, assert_unicode=None, - unicode_error=None, _warn_on_bytestring=False)), - Column('progress', - String(length=255, convert_unicode=False, assert_unicode=None, - unicode_error=None, _warn_on_bytestring=False)), - Column('volume_size', Integer()), - Column('scheduled_at', DateTime(timezone=False)), - Column('display_name', - String(length=255, convert_unicode=False, assert_unicode=None, - unicode_error=None, _warn_on_bytestring=False)), - Column('display_description', - String(length=255, convert_unicode=False, assert_unicode=None, - unicode_error=None, _warn_on_bytestring=False)) - ) - - -def upgrade(migrate_engine): - # Upgrade operations go here. Don't create your own engine; - # bind migrate_engine to your metadata - meta.bind = migrate_engine - - try: - snapshots.create() - except Exception: - logging.info(repr(snapshots)) - logging.exception('Exception while creating table') - meta.drop_all(tables=[snapshots]) - raise - - -def downgrade(migrate_engine): - # Operations to reverse the above upgrade go here. - snapshots.drop() diff --git a/nova/db/sqlalchemy/migrate_repo/versions/016_add_snapshot_id_to_volumes.py b/nova/db/sqlalchemy/migrate_repo/versions/016_add_snapshot_id_to_volumes.py deleted file mode 100644 index 0a50123bf..000000000 --- a/nova/db/sqlalchemy/migrate_repo/versions/016_add_snapshot_id_to_volumes.py +++ /dev/null @@ -1,48 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 MORITA Kazutaka. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -from sqlalchemy import * -from migrate import * - -from nova import log as logging - - -meta = MetaData() - - -# Table stub-definitions -# Just for the ForeignKey and column creation to succeed, these are not the -# actual definitions of instances or services. -# -volumes = Table('volumes', meta, - Column('id', Integer(), primary_key=True, nullable=False), - ) - -# -# New Column -# - -snapshot_id = Column('snapshot_id', Integer()) - - -def upgrade(migrate_engine): - # Upgrade operations go here. Don't create your own engine; - # bind migrate_engine to your metadata - meta.bind = migrate_engine - - # Add columns to existing tables - volumes.create_column(snapshot_id) diff --git a/nova/db/sqlalchemy/migrate_repo/versions/016_add_volume_snapshot_support.py b/nova/db/sqlalchemy/migrate_repo/versions/016_add_volume_snapshot_support.py new file mode 100644 index 000000000..288f63e72 --- /dev/null +++ b/nova/db/sqlalchemy/migrate_repo/versions/016_add_volume_snapshot_support.py @@ -0,0 +1,71 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 MORITA Kazutaka. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from sqlalchemy import * +from migrate import * + +from nova import log as logging + +meta = MetaData() + +snapshots = Table('snapshots', meta, + Column('created_at', DateTime(timezone=False)), + Column('updated_at', DateTime(timezone=False)), + Column('deleted_at', DateTime(timezone=False)), + Column('deleted', Boolean(create_constraint=True, name=None)), + Column('id', Integer(), primary_key=True, nullable=False), + Column('volume_id', Integer(), nullable=False), + Column('user_id', + String(length=255, convert_unicode=False, assert_unicode=None, + unicode_error=None, _warn_on_bytestring=False)), + Column('project_id', + String(length=255, convert_unicode=False, assert_unicode=None, + unicode_error=None, _warn_on_bytestring=False)), + Column('status', + String(length=255, convert_unicode=False, assert_unicode=None, + unicode_error=None, _warn_on_bytestring=False)), + Column('progress', + String(length=255, convert_unicode=False, assert_unicode=None, + unicode_error=None, _warn_on_bytestring=False)), + Column('volume_size', Integer()), + Column('scheduled_at', DateTime(timezone=False)), + Column('display_name', + String(length=255, convert_unicode=False, assert_unicode=None, + unicode_error=None, _warn_on_bytestring=False)), + Column('display_description', + String(length=255, convert_unicode=False, assert_unicode=None, + unicode_error=None, _warn_on_bytestring=False)) + ) + + +def upgrade(migrate_engine): + # Upgrade operations go here. Don't create your own engine; + # bind migrate_engine to your metadata + meta.bind = migrate_engine + + try: + snapshots.create() + except Exception: + logging.info(repr(snapshots)) + logging.exception('Exception while creating table') + meta.drop_all(tables=[snapshots]) + raise + + +def downgrade(migrate_engine): + # Operations to reverse the above upgrade go here. + snapshots.drop() diff --git a/nova/db/sqlalchemy/migrate_repo/versions/017_add_snapshot_id_to_volumes.py b/nova/db/sqlalchemy/migrate_repo/versions/017_add_snapshot_id_to_volumes.py new file mode 100644 index 000000000..0a50123bf --- /dev/null +++ b/nova/db/sqlalchemy/migrate_repo/versions/017_add_snapshot_id_to_volumes.py @@ -0,0 +1,48 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 MORITA Kazutaka. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from sqlalchemy import * +from migrate import * + +from nova import log as logging + + +meta = MetaData() + + +# Table stub-definitions +# Just for the ForeignKey and column creation to succeed, these are not the +# actual definitions of instances or services. +# +volumes = Table('volumes', meta, + Column('id', Integer(), primary_key=True, nullable=False), + ) + +# +# New Column +# + +snapshot_id = Column('snapshot_id', Integer()) + + +def upgrade(migrate_engine): + # Upgrade operations go here. Don't create your own engine; + # bind migrate_engine to your metadata + meta.bind = migrate_engine + + # Add columns to existing tables + volumes.create_column(snapshot_id) -- cgit From 6d04d6e17f753e0573d37992576dedccccf9db93 Mon Sep 17 00:00:00 2001 From: Sandy Walsh Date: Fri, 13 May 2011 10:43:43 -0700 Subject: started on integrating HostFilter --- nova/scheduler/host_filter.py | 18 ++++++++++++++++++ nova/scheduler/zone_aware_scheduler.py | 16 ++++++++++++---- 2 files changed, 30 insertions(+), 4 deletions(-) (limited to 'nova') diff --git a/nova/scheduler/host_filter.py b/nova/scheduler/host_filter.py index 483f3225c..17f63d4a0 100644 --- a/nova/scheduler/host_filter.py +++ b/nova/scheduler/host_filter.py @@ -286,3 +286,21 @@ def choose_driver(driver_name=None): if "%s.%s" % (driver.__module__, driver.__name__) == driver_name: return driver() raise exception.SchedulerHostFilterDriverNotFound(driver_name=driver_name) + + +class HostFilterScheduler(ZoneAwareScheduler): + """The HostFilterScheduler uses the HostFilter drivers to filter + hosts for weighing. The particular driver used may be passed in + as an argument or the default will be used.""" + + def filter_hosts(self, num, specs): + """Filter the full host list (from the ZoneManager)""" + driver_name = specs.get("filter_driver", None) + driver = host_filter.choose_driver(driver_name) + + # TODO(sandy): We're only using InstanceType-based specs + # currently. Later we'll need to snoop for more detailed + # host filter requests. + instance_type = specs['instance_type'] + query = driver.instance_type_to_filter(query) + return driver.filter_hosts(self.zone_manager, query) diff --git a/nova/scheduler/zone_aware_scheduler.py b/nova/scheduler/zone_aware_scheduler.py index b3d230bd2..fde8b6792 100644 --- a/nova/scheduler/zone_aware_scheduler.py +++ b/nova/scheduler/zone_aware_scheduler.py @@ -25,6 +25,7 @@ import operator from nova import log as logging from nova.scheduler import api from nova.scheduler import driver +from nova.scheduler import host_filter LOG = logging.getLogger('nova.scheduler.zone_aware_scheduler') @@ -36,7 +37,7 @@ class ZoneAwareScheduler(driver.Scheduler): """Call novaclient zone method. Broken out for testing.""" return api.call_zone_method(context, method, specs=specs) - def schedule_run_instance(self, context, topic='compute', specs={}, + def schedule_run_instance(self, context, instance_id, instance_type, *args, **kwargs): """This method is called from nova.compute.api to provision an instance. However we need to look at the parameters being @@ -46,6 +47,9 @@ class ZoneAwareScheduler(driver.Scheduler): to simply create the instance (either in this zone or a child zone).""" + # TODO(sandy): We'll have to look for richer specs at some point. + specs = instance_type + if 'blob' in specs: return self.provision_instance(context, topic, specs) @@ -58,7 +62,7 @@ class ZoneAwareScheduler(driver.Scheduler): """Create the requested instance in this Zone or a child zone.""" pass - def select(self, context, *args, **kwargs): + def select(self, context, specs, *args, **kwargs): """Select returns a list of weights and zone/host information corresponding to the best hosts to service the request. Any child zone information has been encrypted so as not to reveal @@ -80,9 +84,13 @@ class ZoneAwareScheduler(driver.Scheduler): ordered by their fitness. """ - #TODO(sandy): extract these from args. + if topic != "compute": + raise NotImplemented(_("Zone Aware Scheduler only understands " + "Compute nodes (for now)")) + + specs = args['instance_type'] + #TODO(sandy): how to infer this from OS API params? num_instances = 1 - specs = {} # Filter local hosts based on requirements ... host_list = self.filter_hosts(num_instances, specs) -- cgit From d44299be90bbfcac5f8de1e1264b81fbb0bfa5e2 Mon Sep 17 00:00:00 2001 From: Masanori Itoh Date: Tue, 17 May 2011 01:00:16 +0900 Subject: Add vnc_keymap flag and enable setting keymap for vnc console. --- nova/virt/libvirt.xml.template | 2 +- nova/virt/libvirt_conn.py | 1 + nova/vnc/__init__.py | 2 ++ 3 files changed, 4 insertions(+), 1 deletion(-) (limited to 'nova') diff --git a/nova/virt/libvirt.xml.template b/nova/virt/libvirt.xml.template index de2497a76..20986d4d5 100644 --- a/nova/virt/libvirt.xml.template +++ b/nova/virt/libvirt.xml.template @@ -116,7 +116,7 @@ #if $getVar('vncserver_host', False) - + #end if diff --git a/nova/virt/libvirt_conn.py b/nova/virt/libvirt_conn.py index 555e44ce2..7552c9488 100644 --- a/nova/virt/libvirt_conn.py +++ b/nova/virt/libvirt_conn.py @@ -1022,6 +1022,7 @@ class LibvirtConnection(driver.ComputeDriver): if FLAGS.vnc_enabled: if FLAGS.libvirt_type != 'lxc': xml_info['vncserver_host'] = FLAGS.vncserver_host + xml_info['vnc_keymap'] = FLAGS.vnc_keymap if not rescue: if instance['kernel_id']: xml_info['kernel'] = xml_info['basepath'] + "/kernel" diff --git a/nova/vnc/__init__.py b/nova/vnc/__init__.py index b5b00e44e..859bfd65f 100644 --- a/nova/vnc/__init__.py +++ b/nova/vnc/__init__.py @@ -32,3 +32,5 @@ flags.DEFINE_string('vncserver_host', '0.0.0.0', 'the host interface on which vnc server should listen') flags.DEFINE_bool('vnc_enabled', True, 'enable vnc related features') +flags.DEFINE_string('vnc_keymap', 'en-us', + 'keymap for vnc') -- cgit From 02bba6a8f49b924e9b5b0e69124afd953e8cc3ae Mon Sep 17 00:00:00 2001 From: Sandy Walsh Date: Mon, 16 May 2011 15:37:25 -0700 Subject: basic call going through --- nova/compute/api.py | 7 ++++++- nova/scheduler/host_filter.py | 19 ++++++++++--------- nova/scheduler/zone_aware_scheduler.py | 32 +++++++++++++++----------------- nova/virt/fake.py | 24 ++++++++++++++++++++++++ 4 files changed, 55 insertions(+), 27 deletions(-) (limited to 'nova') diff --git a/nova/compute/api.py b/nova/compute/api.py index 8a7c713a2..f57d5cd90 100644 --- a/nova/compute/api.py +++ b/nova/compute/api.py @@ -259,7 +259,12 @@ class API(base.Base): {"method": "run_instance", "args": {"topic": FLAGS.compute_topic, "instance_id": instance_id, - "instance_type": instance_type, + "request_spec": { + 'instance_type': instance_type, + 'filter_driver': + 'nova.scheduler.host_filter.' + 'InstanceTypeFilter' + }, "availability_zone": availability_zone, "injected_files": injected_files}}) diff --git a/nova/scheduler/host_filter.py b/nova/scheduler/host_filter.py index 17f63d4a0..a47e41da4 100644 --- a/nova/scheduler/host_filter.py +++ b/nova/scheduler/host_filter.py @@ -42,6 +42,7 @@ from nova import exception from nova import flags from nova import log as logging from nova import utils +from nova.scheduler import zone_aware_scheduler LOG = logging.getLogger('nova.scheduler.host_filter') @@ -83,8 +84,8 @@ class AllHostsFilter(HostFilter): for host, services in zone_manager.service_states.iteritems()] -class FlavorFilter(HostFilter): - """HostFilter driver hard-coded to work with flavors.""" +class InstanceTypeFilter(HostFilter): + """HostFilter driver hard-coded to work with InstanceType records.""" def instance_type_to_filter(self, instance_type): """Use instance_type to filter hosts.""" @@ -271,7 +272,7 @@ class JsonFilter(HostFilter): return hosts -DRIVERS = [AllHostsFilter, FlavorFilter, JsonFilter] +DRIVERS = [AllHostsFilter, InstanceTypeFilter, JsonFilter] def choose_driver(driver_name=None): @@ -288,19 +289,19 @@ def choose_driver(driver_name=None): raise exception.SchedulerHostFilterDriverNotFound(driver_name=driver_name) -class HostFilterScheduler(ZoneAwareScheduler): +class HostFilterScheduler(zone_aware_scheduler.ZoneAwareScheduler): """The HostFilterScheduler uses the HostFilter drivers to filter hosts for weighing. The particular driver used may be passed in as an argument or the default will be used.""" - def filter_hosts(self, num, specs): + def filter_hosts(self, num, request_spec): """Filter the full host list (from the ZoneManager)""" - driver_name = specs.get("filter_driver", None) - driver = host_filter.choose_driver(driver_name) + driver_name = request_spec.get("filter_driver", None) + driver = choose_driver(driver_name) # TODO(sandy): We're only using InstanceType-based specs # currently. Later we'll need to snoop for more detailed # host filter requests. - instance_type = specs['instance_type'] - query = driver.instance_type_to_filter(query) + instance_type = request_spec['instance_type'] + query = driver.instance_type_to_filter(instance_type) return driver.filter_hosts(self.zone_manager, query) diff --git a/nova/scheduler/zone_aware_scheduler.py b/nova/scheduler/zone_aware_scheduler.py index fde8b6792..f9c5f65f3 100644 --- a/nova/scheduler/zone_aware_scheduler.py +++ b/nova/scheduler/zone_aware_scheduler.py @@ -25,7 +25,6 @@ import operator from nova import log as logging from nova.scheduler import api from nova.scheduler import driver -from nova.scheduler import host_filter LOG = logging.getLogger('nova.scheduler.zone_aware_scheduler') @@ -37,7 +36,7 @@ class ZoneAwareScheduler(driver.Scheduler): """Call novaclient zone method. Broken out for testing.""" return api.call_zone_method(context, method, specs=specs) - def schedule_run_instance(self, context, instance_id, instance_type, + def schedule_run_instance(self, context, instance_id, request_spec, *args, **kwargs): """This method is called from nova.compute.api to provision an instance. However we need to look at the parameters being @@ -48,13 +47,12 @@ class ZoneAwareScheduler(driver.Scheduler): a child zone).""" # TODO(sandy): We'll have to look for richer specs at some point. - specs = instance_type - if 'blob' in specs: - return self.provision_instance(context, topic, specs) + if 'blob' in request_spec: + return self.provision_instance(context, topic, request_spec) # Create build plan and provision ... - build_plan = self.select(context, specs) + build_plan = self.select(context, request_spec) for item in build_plan: self.provision_instance(context, topic, item) @@ -62,24 +60,24 @@ class ZoneAwareScheduler(driver.Scheduler): """Create the requested instance in this Zone or a child zone.""" pass - def select(self, context, specs, *args, **kwargs): + def select(self, context, request_spec, *args, **kwargs): """Select returns a list of weights and zone/host information corresponding to the best hosts to service the request. Any child zone information has been encrypted so as not to reveal anything about the children.""" - return self._schedule(context, "compute", *args, **kwargs) + return self._schedule(context, "compute", request_spec, *args, **kwargs) - def schedule(self, context, topic, *args, **kwargs): + def schedule(self, context, topic, request_spec, *args, **kwargs): """The schedule() contract requires we return the one best-suited host for this request. """ - res = self._schedule(context, topic, *args, **kwargs) + res = self._schedule(context, topic, request_spec, *args, **kwargs) # TODO(sirp): should this be a host object rather than a weight-dict? if not res: raise driver.NoValidHost(_('No hosts were available')) return res[0] - def _schedule(self, context, topic, *args, **kwargs): + def _schedule(self, context, topic, request_spec, *args, **kwargs): """Returns a list of hosts that meet the required specs, ordered by their fitness. """ @@ -88,20 +86,20 @@ class ZoneAwareScheduler(driver.Scheduler): raise NotImplemented(_("Zone Aware Scheduler only understands " "Compute nodes (for now)")) - specs = args['instance_type'] + LOG.debug("specs = %s, ARGS = %s" % (request_spec, args, )) #TODO(sandy): how to infer this from OS API params? num_instances = 1 # Filter local hosts based on requirements ... - host_list = self.filter_hosts(num_instances, specs) + host_list = self.filter_hosts(num_instances, request_spec) # then weigh the selected hosts. # weighted = [{weight=weight, name=hostname}, ...] - weighted = self.weigh_hosts(num_instances, specs, host_list) + weighted = self.weigh_hosts(num_instances, request_spec, host_list) # Next, tack on the best weights from the child zones ... child_results = self._call_zone_method(context, "select", - specs=specs) + specs=request_spec) for child_zone, result in child_results: for weighting in result: # Remember the child_zone so we can get back to @@ -116,12 +114,12 @@ class ZoneAwareScheduler(driver.Scheduler): weighted.sort(key=operator.itemgetter('weight')) return weighted - def filter_hosts(self, num, specs): + def filter_hosts(self, num, request_spec): """Derived classes must override this method and return a list of hosts in [(hostname, capability_dict)] format.""" raise NotImplemented() - def weigh_hosts(self, num, specs, hosts): + def weigh_hosts(self, num, request_spec, hosts): """Derived classes must override this method and return a lists of hosts in [{weight, hostname}] format.""" raise NotImplemented() diff --git a/nova/virt/fake.py b/nova/virt/fake.py index 5ac376e46..bf87e5ced 100644 --- a/nova/virt/fake.py +++ b/nova/virt/fake.py @@ -82,6 +82,20 @@ class FakeConnection(driver.ComputeDriver): def __init__(self): self.instances = {} + self.host_status = {'host_name-description': 'Fake Host', + 'host_hostname': 'fake-mini', + 'host_memory_total': 8000000000, + 'host_memory_overhead': 10000000, + 'host_memory_free': 7900000000, + 'host_memory_free_computed': 7900000000, + 'host_other-config': {}, + 'host_ip_address': '192.168.1.109', + 'host_cpu_info': {}, + 'disk_available': 500000000000, + 'disk_total': 600000000000, + 'disk_used': 100000000000, + 'host_uuid': 'cedb9b39-9388-41df-8891-c5c9a0c0fe5f', + 'host_name-label': 'fake-mini'} @classmethod def instance(cls): @@ -456,3 +470,13 @@ class FakeConnection(driver.ComputeDriver): def test_remove_vm(self, instance_name): """ Removes the named VM, as if it crashed. For testing""" self.instances.pop(instance_name) + + def update_host_status(self): + """Return fake Host Status of ram, disk, network.""" + return self.host_status + + def get_host_stats(self, refresh=False): + """Return fake Host Status of ram, disk, network.""" + return self.host_status + + -- cgit From 7ed85c9ee57190589efcb22819783d6faf973cc3 Mon Sep 17 00:00:00 2001 From: Sandy Walsh Date: Tue, 17 May 2011 05:27:50 -0700 Subject: tests fixed and pep8'ed --- nova/scheduler/host_filter.py | 10 +++++++--- nova/scheduler/manager.py | 3 ++- nova/scheduler/zone_aware_scheduler.py | 11 ++++++++--- nova/tests/test_host_filter.py | 11 ++++++----- nova/virt/fake.py | 2 -- 5 files changed, 23 insertions(+), 14 deletions(-) (limited to 'nova') diff --git a/nova/scheduler/host_filter.py b/nova/scheduler/host_filter.py index 8519b8b51..2b0d9af77 100644 --- a/nova/scheduler/host_filter.py +++ b/nova/scheduler/host_filter.py @@ -292,11 +292,15 @@ def choose_driver(driver_name=None): class HostFilterScheduler(zone_aware_scheduler.ZoneAwareScheduler): """The HostFilterScheduler uses the HostFilter drivers to filter hosts for weighing. The particular driver used may be passed in - as an argument or the default will be used.""" + as an argument or the default will be used. + + request_spec = {'filter_driver': , + 'instance_type': } + """ def filter_hosts(self, num, request_spec): """Filter the full host list (from the ZoneManager)""" - driver_name = request_spec.get("filter_driver", None) + driver_name = request_spec.get('filter_driver', None) driver = choose_driver(driver_name) # TODO(sandy): We're only using InstanceType-based specs @@ -309,4 +313,4 @@ class HostFilterScheduler(zone_aware_scheduler.ZoneAwareScheduler): def weigh_hosts(self, num, request_spec, hosts): """Derived classes must override this method and return a lists of hosts in [{weight, hostname}] format.""" - return [] + return [dict(weight=1, hostname=hostname) for host, caps in hosts] diff --git a/nova/scheduler/manager.py b/nova/scheduler/manager.py index 60f98b082..bd40e73c0 100644 --- a/nova/scheduler/manager.py +++ b/nova/scheduler/manager.py @@ -84,7 +84,8 @@ class SchedulerManager(manager.Manager): host = self.driver.schedule(elevated, topic, *args, **kwargs) if not host: - LOG.debug(_("%(topic)s %(method)s handled in Scheduler") % locals()) + LOG.debug(_("%(topic)s %(method)s handled in Scheduler") + % locals()) return rpc.cast(context, diff --git a/nova/scheduler/zone_aware_scheduler.py b/nova/scheduler/zone_aware_scheduler.py index 2fc5f1f87..614b1bb89 100644 --- a/nova/scheduler/zone_aware_scheduler.py +++ b/nova/scheduler/zone_aware_scheduler.py @@ -59,20 +59,25 @@ class ZoneAwareScheduler(driver.Scheduler): for item in build_plan: self.provision_instance(context, topic, item) + # Returning None short-circuits the routing to Compute (since + # we've already done it here) + return None + def provision_instance(context, topic, item): """Create the requested instance in this Zone or a child zone.""" - pass + return None def select(self, context, request_spec, *args, **kwargs): """Select returns a list of weights and zone/host information corresponding to the best hosts to service the request. Any child zone information has been encrypted so as not to reveal anything about the children.""" - return self._schedule(context, "compute", request_spec, *args, **kwargs) + return self._schedule(context, "compute", request_spec, + *args, **kwargs) # TODO(sandy): We're only focused on compute instances right now, # so we don't implement the default "schedule()" method required - # of Schedulers. + # of Schedulers. def schedule(self, context, topic, request_spec, *args, **kwargs): """The schedule() contract requires we return the one best-suited host for this request. diff --git a/nova/tests/test_host_filter.py b/nova/tests/test_host_filter.py index c029d41e6..dd2325cc6 100644 --- a/nova/tests/test_host_filter.py +++ b/nova/tests/test_host_filter.py @@ -85,9 +85,9 @@ class HostFilterTestCase(test.TestCase): 'nova.scheduler.host_filter.AllHostsFilter') # Test valid driver ... driver = host_filter.choose_driver( - 'nova.scheduler.host_filter.FlavorFilter') + 'nova.scheduler.host_filter.InstanceTypeFilter') self.assertEquals(driver._full_name(), - 'nova.scheduler.host_filter.FlavorFilter') + 'nova.scheduler.host_filter.InstanceTypeFilter') # Test invalid driver ... try: host_filter.choose_driver('does not exist') @@ -103,11 +103,12 @@ class HostFilterTestCase(test.TestCase): for host, capabilities in hosts: self.assertTrue(host.startswith('host')) - def test_flavor_driver(self): - driver = host_filter.FlavorFilter() + def test_instance_type_driver(self): + driver = host_filter.InstanceTypeFilter() # filter all hosts that can support 50 ram and 500 disk name, cooked = driver.instance_type_to_filter(self.instance_type) - self.assertEquals('nova.scheduler.host_filter.FlavorFilter', name) + self.assertEquals('nova.scheduler.host_filter.InstanceTypeFilter', + name) hosts = driver.filter_hosts(self.zone_manager, cooked) self.assertEquals(6, len(hosts)) just_hosts = [host for host, caps in hosts] diff --git a/nova/virt/fake.py b/nova/virt/fake.py index bf87e5ced..3bd9fbc93 100644 --- a/nova/virt/fake.py +++ b/nova/virt/fake.py @@ -478,5 +478,3 @@ class FakeConnection(driver.ComputeDriver): def get_host_stats(self, refresh=False): """Return fake Host Status of ram, disk, network.""" return self.host_status - - -- cgit From effa4b37fae0e6fef993ffd2892bb77c0e7245f1 Mon Sep 17 00:00:00 2001 From: Sandy Walsh Date: Tue, 17 May 2011 05:43:06 -0700 Subject: ugh, fixed again --- nova/tests/test_zone_aware_scheduler.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) (limited to 'nova') diff --git a/nova/tests/test_zone_aware_scheduler.py b/nova/tests/test_zone_aware_scheduler.py index fdcde34c9..37169fb97 100644 --- a/nova/tests/test_zone_aware_scheduler.py +++ b/nova/tests/test_zone_aware_scheduler.py @@ -116,4 +116,6 @@ class ZoneAwareSchedulerTestCase(test.TestCase): sched.set_zone_manager(zm) fake_context = {} - self.assertRaises(driver.NoValidHost, sched.schedule, fake_context, {}) + self.assertRaises(driver.NoValidHost, sched.schedule_run_instance, + fake_context, 1, + dict(host_filter=None, instance_type={})) -- cgit From 84e8893c08cced5f7097b5c90e21a8a06740b3ab Mon Sep 17 00:00:00 2001 From: Sandy Walsh Date: Tue, 17 May 2011 07:49:12 -0700 Subject: provision working correctly now --- nova/scheduler/host_filter.py | 19 ++++++++++--------- nova/scheduler/zone_aware_scheduler.py | 26 +++++++++++++++++++++----- nova/virt/fake.py | 7 ++++--- 3 files changed, 35 insertions(+), 17 deletions(-) (limited to 'nova') diff --git a/nova/scheduler/host_filter.py b/nova/scheduler/host_filter.py index 2b0d9af77..92ec827d3 100644 --- a/nova/scheduler/host_filter.py +++ b/nova/scheduler/host_filter.py @@ -99,9 +99,10 @@ class InstanceTypeFilter(HostFilter): capabilities = services.get('compute', {}) host_ram_mb = capabilities['host_memory_free'] disk_bytes = capabilities['disk_available'] - if host_ram_mb >= instance_type['memory_mb'] and \ - disk_bytes >= instance_type['local_gb']: - selected_hosts.append((host, capabilities)) + spec_ram = instance_type['memory_mb'] + spec_disk = instance_type['local_gb'] + if host_ram_mb >= spec_ram and disk_bytes >= spec_disk: + selected_hosts.append((host, capabilities)) return selected_hosts #host entries (currently) are like: @@ -110,15 +111,15 @@ class InstanceTypeFilter(HostFilter): # 'host_memory_total': 8244539392, # 'host_memory_overhead': 184225792, # 'host_memory_free': 3868327936, -# 'host_memory_free_computed': 3840843776}, -# 'host_other-config': {}, +# 'host_memory_free_computed': 3840843776, +# 'host_other_config': {}, # 'host_ip_address': '192.168.1.109', # 'host_cpu_info': {}, # 'disk_available': 32954957824, # 'disk_total': 50394562560, -# 'disk_used': 17439604736}, +# 'disk_used': 17439604736, # 'host_uuid': 'cedb9b39-9388-41df-8891-c5c9a0c0fe5f', -# 'host_name-label': 'xs-mini'} +# 'host_name_label': 'xs-mini'} # instance_type table has: #name = Column(String(255), unique=True) @@ -307,10 +308,10 @@ class HostFilterScheduler(zone_aware_scheduler.ZoneAwareScheduler): # currently. Later we'll need to snoop for more detailed # host filter requests. instance_type = request_spec['instance_type'] - query = driver.instance_type_to_filter(instance_type) + name, query = driver.instance_type_to_filter(instance_type) return driver.filter_hosts(self.zone_manager, query) def weigh_hosts(self, num, request_spec, hosts): """Derived classes must override this method and return a lists of hosts in [{weight, hostname}] format.""" - return [dict(weight=1, hostname=hostname) for host, caps in hosts] + return [dict(weight=1, hostname=host) for host, caps in hosts] diff --git a/nova/scheduler/zone_aware_scheduler.py b/nova/scheduler/zone_aware_scheduler.py index 614b1bb89..3ebb4caef 100644 --- a/nova/scheduler/zone_aware_scheduler.py +++ b/nova/scheduler/zone_aware_scheduler.py @@ -22,6 +22,8 @@ across zones. There are two expansion points to this class for: import operator +from nova import db +from nova import rpc from nova import log as logging from nova.scheduler import api from nova.scheduler import driver @@ -49,7 +51,8 @@ class ZoneAwareScheduler(driver.Scheduler): # TODO(sandy): We'll have to look for richer specs at some point. if 'blob' in request_spec: - return self.provision_instance(context, topic, request_spec) + return self.provision_resource(context, request_spec, + instance_id, kwargs) # Create build plan and provision ... build_plan = self.select(context, request_spec) @@ -57,14 +60,28 @@ class ZoneAwareScheduler(driver.Scheduler): raise driver.NoValidHost(_('No hosts were available')) for item in build_plan: - self.provision_instance(context, topic, item) + self.provision_resource(context, item, instance_id, kwargs) # Returning None short-circuits the routing to Compute (since # we've already done it here) return None - def provision_instance(context, topic, item): - """Create the requested instance in this Zone or a child zone.""" + def provision_resource(self, context, item, instance_id, kwargs): + """Create the requested resource in this Zone or a child zone.""" + if "hostname" in item: + host = item['hostname'] + kwargs['instance_id'] = instance_id + rpc.cast(context, + db.queue_get_for(context, "compute", host), + {"method": "run_instance", + "args": kwargs}) + LOG.debug(_("Casted to compute %(host)s for run_instance") + % locals()) + else: + # TODO(sandy) Provision in child zone ... + LOG.warning(_("Provision to Child Zone not supported (yet)") + % locals()) + pass return None def select(self, context, request_spec, *args, **kwargs): @@ -93,7 +110,6 @@ class ZoneAwareScheduler(driver.Scheduler): raise NotImplemented(_("Zone Aware Scheduler only understands " "Compute nodes (for now)")) - LOG.debug("specs = %s, ARGS = %s" % (request_spec, args, )) #TODO(sandy): how to infer this from OS API params? num_instances = 1 diff --git a/nova/virt/fake.py b/nova/virt/fake.py index 3bd9fbc93..0225797d7 100644 --- a/nova/virt/fake.py +++ b/nova/virt/fake.py @@ -82,20 +82,21 @@ class FakeConnection(driver.ComputeDriver): def __init__(self): self.instances = {} - self.host_status = {'host_name-description': 'Fake Host', + self.host_status = { + 'host_name-description': 'Fake Host', 'host_hostname': 'fake-mini', 'host_memory_total': 8000000000, 'host_memory_overhead': 10000000, 'host_memory_free': 7900000000, 'host_memory_free_computed': 7900000000, - 'host_other-config': {}, + 'host_other_config': {}, 'host_ip_address': '192.168.1.109', 'host_cpu_info': {}, 'disk_available': 500000000000, 'disk_total': 600000000000, 'disk_used': 100000000000, 'host_uuid': 'cedb9b39-9388-41df-8891-c5c9a0c0fe5f', - 'host_name-label': 'fake-mini'} + 'host_name_label': 'fake-mini'} @classmethod def instance(cls): -- cgit From b66c689afc5923702b3d6d27a5c8f12f6749b07d Mon Sep 17 00:00:00 2001 From: Sandy Walsh Date: Tue, 17 May 2011 07:52:02 -0700 Subject: provision_resource no longer returns value --- nova/scheduler/zone_aware_scheduler.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) (limited to 'nova') diff --git a/nova/scheduler/zone_aware_scheduler.py b/nova/scheduler/zone_aware_scheduler.py index 3ebb4caef..2050c8918 100644 --- a/nova/scheduler/zone_aware_scheduler.py +++ b/nova/scheduler/zone_aware_scheduler.py @@ -51,8 +51,8 @@ class ZoneAwareScheduler(driver.Scheduler): # TODO(sandy): We'll have to look for richer specs at some point. if 'blob' in request_spec: - return self.provision_resource(context, request_spec, - instance_id, kwargs) + self.provision_resource(context, request_spec, instance_id, kwargs) + return None # Create build plan and provision ... build_plan = self.select(context, request_spec) @@ -82,7 +82,6 @@ class ZoneAwareScheduler(driver.Scheduler): LOG.warning(_("Provision to Child Zone not supported (yet)") % locals()) pass - return None def select(self, context, request_spec, *args, **kwargs): """Select returns a list of weights and zone/host information -- cgit From 40f15a6ffb0d9ae965f9c7c7289654f323f2775f Mon Sep 17 00:00:00 2001 From: Sandy Walsh Date: Tue, 17 May 2011 13:01:28 -0700 Subject: pep8 --- nova/scheduler/zone_aware_scheduler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'nova') diff --git a/nova/scheduler/zone_aware_scheduler.py b/nova/scheduler/zone_aware_scheduler.py index 2050c8918..dc18fc427 100644 --- a/nova/scheduler/zone_aware_scheduler.py +++ b/nova/scheduler/zone_aware_scheduler.py @@ -78,7 +78,7 @@ class ZoneAwareScheduler(driver.Scheduler): LOG.debug(_("Casted to compute %(host)s for run_instance") % locals()) else: - # TODO(sandy) Provision in child zone ... + # TODO(sandy) Provision in child zone ... LOG.warning(_("Provision to Child Zone not supported (yet)") % locals()) pass -- cgit From e6fc2fc58d2c98f4322e92b26b1031ca362c8724 Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Tue, 17 May 2011 15:14:17 -0700 Subject: add more statuses for ec2 image registration --- nova/image/s3.py | 84 ++++++++++++++++++++++++++++++++++++++------------------ 1 file changed, 58 insertions(+), 26 deletions(-) (limited to 'nova') diff --git a/nova/image/s3.py b/nova/image/s3.py index c38c58d95..673cbf56f 100644 --- a/nova/image/s3.py +++ b/nova/image/s3.py @@ -161,43 +161,75 @@ class S3ImageService(service.BaseImageService): def delayed_create(): """This handles the fetching and decrypting of the part files.""" - parts = [] - for fn_element in manifest.find('image').getiterator('filename'): - part = self._download_file(bucket, fn_element.text, image_path) - parts.append(part) - - # NOTE(vish): this may be suboptimal, should we use cat? - encrypted_filename = os.path.join(image_path, 'image.encrypted') - with open(encrypted_filename, 'w') as combined: - for filename in parts: - with open(filename) as part: - shutil.copyfileobj(part, combined) - - metadata['properties']['image_state'] = 'decrypting' + metadata['properties']['image_state'] = 'downloading' self.service.update(context, image_id, metadata) - hex_key = manifest.find('image/ec2_encrypted_key').text - encrypted_key = binascii.a2b_hex(hex_key) - hex_iv = manifest.find('image/ec2_encrypted_iv').text - encrypted_iv = binascii.a2b_hex(hex_iv) + try: + parts = [] + elements = manifest.find('image').getiterator('filename') + for fn_element in elements: + part = self._download_file(bucket, + fn_element.text, + image_path) + parts.append(part) + + # NOTE(vish): this may be suboptimal, should we use cat? + enc_filename = os.path.join(image_path, 'image.encrypted') + with open(enc_filename, 'w') as combined: + for filename in parts: + with open(filename) as part: + shutil.copyfileobj(part, combined) + + except Exception: + metadata['properties']['image_state'] = 'failed_download' + self.service.update(context, image_id, metadata) + raise - # FIXME(vish): grab key from common service so this can run on - # any host. - cloud_pk = crypto.key_path(context.project_id) + metadata['properties']['image_state'] = 'decrypting' + self.service.update(context, image_id, metadata) - decrypted_filename = os.path.join(image_path, 'image.tar.gz') - self._decrypt_image(encrypted_filename, encrypted_key, - encrypted_iv, cloud_pk, decrypted_filename) + try: + hex_key = manifest.find('image/ec2_encrypted_key').text + encrypted_key = binascii.a2b_hex(hex_key) + hex_iv = manifest.find('image/ec2_encrypted_iv').text + encrypted_iv = binascii.a2b_hex(hex_iv) + + # FIXME(vish): grab key from common service so this can run on + # any host. + cloud_pk = crypto.key_path(context.project_id) + + dec_filename = os.path.join(image_path, 'image.tar.gz') + self._decrypt_image(enc_filename, encrypted_key, + encrypted_iv, cloud_pk, + dec_filename) + except Exception: + metadata['properties']['image_state'] = 'failed_decrypt' + self.service.update(context, image_id, metadata) + raise metadata['properties']['image_state'] = 'untarring' self.service.update(context, image_id, metadata) - unz_filename = self._untarzip_image(image_path, decrypted_filename) + try: + unz_filename = self._untarzip_image(image_path, dec_filename) + except Exception: + metadata['properties']['image_state'] = 'failed_untar' + self.service.update(context, image_id, metadata) + raise metadata['properties']['image_state'] = 'uploading' - with open(unz_filename) as image_file: - self.service.update(context, image_id, metadata, image_file) + self.service.update(context, image_id, metadata) + try: + with open(unz_filename) as image_file: + self.service.update(context, image_id, + metadata, image_file) + except Exception: + metadata['properties']['image_state'] = 'failed_upload' + self.service.update(context, image_id, metadata) + raise + metadata['properties']['image_state'] = 'available' + metadata['status'] = 'active' self.service.update(context, image_id, metadata) shutil.rmtree(image_path) -- cgit From 714fe839580d4858417592608e4bf95ac26cf5d1 Mon Sep 17 00:00:00 2001 From: Sandy Walsh Date: Mon, 23 May 2011 10:39:50 -0700 Subject: get rid of all mention of drivers ... it's filter only now --- nova/exception.py | 5 +-- nova/scheduler/host_filter.py | 55 +++++++++++++------------ nova/tests/test_host_filter.py | 93 ++++++++++++++++++++---------------------- 3 files changed, 75 insertions(+), 78 deletions(-) (limited to 'nova') diff --git a/nova/exception.py b/nova/exception.py index 56c20d111..dde39f29b 100644 --- a/nova/exception.py +++ b/nova/exception.py @@ -465,9 +465,8 @@ class ZoneNotFound(NotFound): message = _("Zone %(zone_id)s could not be found.") -class SchedulerHostFilterDriverNotFound(NotFound): - message = _("Scheduler Host Filter Driver %(driver_name)s could" - " not be found.") +class SchedulerHostFilterNotFound(NotFound): + message = _("Scheduler Host Filter %(filter_name)s could not be found.") class InstanceMetadataNotFound(NotFound): diff --git a/nova/scheduler/host_filter.py b/nova/scheduler/host_filter.py index 92ec827d3..d9771754a 100644 --- a/nova/scheduler/host_filter.py +++ b/nova/scheduler/host_filter.py @@ -14,8 +14,8 @@ # under the License. """ -Host Filter is a driver mechanism for requesting instance resources. -Three drivers are included: AllHosts, Flavor & JSON. AllHosts just +Host Filter is a mechanism for requesting instance resources. +Three filters are included: AllHosts, Flavor & JSON. AllHosts just returns the full, unfiltered list of hosts. Flavor is a hard coded matching mechanism based on flavor criteria and JSON is an ad-hoc filter grammar. @@ -47,13 +47,13 @@ from nova.scheduler import zone_aware_scheduler LOG = logging.getLogger('nova.scheduler.host_filter') FLAGS = flags.FLAGS -flags.DEFINE_string('default_host_filter_driver', +flags.DEFINE_string('default_host_filter', 'nova.scheduler.host_filter.AllHostsFilter', - 'Which driver to use for filtering hosts.') + 'Which filter to use for filtering hosts.') class HostFilter(object): - """Base class for host filter drivers.""" + """Base class for host filters.""" def instance_type_to_filter(self, instance_type): """Convert instance_type into a filter for most common use-case.""" @@ -64,12 +64,12 @@ class HostFilter(object): raise NotImplementedError() def _full_name(self): - """module.classname of the filter driver""" + """module.classname of the filter.""" return "%s.%s" % (self.__module__, self.__class__.__name__) class AllHostsFilter(HostFilter): - """NOP host filter driver. Returns all hosts in ZoneManager. + """NOP host filter. Returns all hosts in ZoneManager. This essentially does what the old Scheduler+Chance used to give us.""" @@ -85,7 +85,7 @@ class AllHostsFilter(HostFilter): class InstanceTypeFilter(HostFilter): - """HostFilter driver hard-coded to work with InstanceType records.""" + """HostFilter hard-coded to work with InstanceType records.""" def instance_type_to_filter(self, instance_type): """Use instance_type to filter hosts.""" @@ -133,7 +133,7 @@ class InstanceTypeFilter(HostFilter): class JsonFilter(HostFilter): - """Host Filter driver to allow simple JSON-based grammar for + """Host Filter to allow simple JSON-based grammar for selecting hosts.""" def _equals(self, args): @@ -273,43 +273,44 @@ class JsonFilter(HostFilter): return hosts -DRIVERS = [AllHostsFilter, InstanceTypeFilter, JsonFilter] +FILTERS = [AllHostsFilter, InstanceTypeFilter, JsonFilter] -def choose_driver(driver_name=None): - """Since the caller may specify which driver to use we need +def choose_host_filter(filter_name=None): + """Since the caller may specify which filter to use we need to have an authoritative list of what is permissible. This - function checks the driver name against a predefined set - of acceptable drivers.""" + function checks the filter name against a predefined set + of acceptable filters.""" - if not driver_name: - driver_name = FLAGS.default_host_filter_driver - for driver in DRIVERS: - if "%s.%s" % (driver.__module__, driver.__name__) == driver_name: - return driver() - raise exception.SchedulerHostFilterDriverNotFound(driver_name=driver_name) + if not filter_name: + filter_name = FLAGS.default_host_filter + for filter_class in FILTERS: + if "%s.%s" % (filter_class.__module__, filter_class.__name__) == \ + filter_name: + return filter_class() + raise exception.SchedulerHostFilterNotFound(filter_name=filter_name) class HostFilterScheduler(zone_aware_scheduler.ZoneAwareScheduler): - """The HostFilterScheduler uses the HostFilter drivers to filter - hosts for weighing. The particular driver used may be passed in + """The HostFilterScheduler uses the HostFilter to filter + hosts for weighing. The particular filter used may be passed in as an argument or the default will be used. - request_spec = {'filter_driver': , + request_spec = {'filter_name': , 'instance_type': } """ def filter_hosts(self, num, request_spec): """Filter the full host list (from the ZoneManager)""" - driver_name = request_spec.get('filter_driver', None) - driver = choose_driver(driver_name) + filter_name = request_spec.get('filter_name', None) + host_filter = choose_host_filter(filter_name) # TODO(sandy): We're only using InstanceType-based specs # currently. Later we'll need to snoop for more detailed # host filter requests. instance_type = request_spec['instance_type'] - name, query = driver.instance_type_to_filter(instance_type) - return driver.filter_hosts(self.zone_manager, query) + name, query = host_filter.instance_type_to_filter(instance_type) + return host_filter.filter_hosts(self.zone_manager, query) def weigh_hosts(self, num, request_spec, hosts): """Derived classes must override this method and return diff --git a/nova/tests/test_host_filter.py b/nova/tests/test_host_filter.py index dd2325cc6..07817cc5a 100644 --- a/nova/tests/test_host_filter.py +++ b/nova/tests/test_host_filter.py @@ -13,7 +13,7 @@ # License for the specific language governing permissions and limitations # under the License. """ -Tests For Scheduler Host Filter Drivers. +Tests For Scheduler Host Filters. """ import json @@ -31,7 +31,7 @@ class FakeZoneManager: class HostFilterTestCase(test.TestCase): - """Test case for host filter drivers.""" + """Test case for host filters.""" def _host_caps(self, multiplier): # Returns host capabilities in the following way: @@ -57,8 +57,8 @@ class HostFilterTestCase(test.TestCase): 'host_name-label': 'xs-%s' % multiplier} def setUp(self): - self.old_flag = FLAGS.default_host_filter_driver - FLAGS.default_host_filter_driver = \ + self.old_flag = FLAGS.default_host_filter + FLAGS.default_host_filter = \ 'nova.scheduler.host_filter.AllHostsFilter' self.instance_type = dict(name='tiny', memory_mb=50, @@ -76,52 +76,52 @@ class HostFilterTestCase(test.TestCase): self.zone_manager.service_states = states def tearDown(self): - FLAGS.default_host_filter_driver = self.old_flag + FLAGS.default_host_filter = self.old_flag - def test_choose_driver(self): - # Test default driver ... - driver = host_filter.choose_driver() - self.assertEquals(driver._full_name(), + def test_choose_filter(self): + # Test default filter ... + hf = host_filter.choose_host_filter() + self.assertEquals(hf._full_name(), 'nova.scheduler.host_filter.AllHostsFilter') - # Test valid driver ... - driver = host_filter.choose_driver( + # Test valid filter ... + hf = host_filter.choose_host_filter( 'nova.scheduler.host_filter.InstanceTypeFilter') - self.assertEquals(driver._full_name(), + self.assertEquals(hf._full_name(), 'nova.scheduler.host_filter.InstanceTypeFilter') - # Test invalid driver ... + # Test invalid filter ... try: - host_filter.choose_driver('does not exist') - self.fail("Should not find driver") - except exception.SchedulerHostFilterDriverNotFound: + host_filter.choose_host_filter('does not exist') + self.fail("Should not find host filter.") + except exception.SchedulerHostFilterNotFound: pass - def test_all_host_driver(self): - driver = host_filter.AllHostsFilter() - cooked = driver.instance_type_to_filter(self.instance_type) - hosts = driver.filter_hosts(self.zone_manager, cooked) + def test_all_host_filter(self): + hf = host_filter.AllHostsFilter() + cooked = hf.instance_type_to_filter(self.instance_type) + hosts = hf.filter_hosts(self.zone_manager, cooked) self.assertEquals(10, len(hosts)) for host, capabilities in hosts: self.assertTrue(host.startswith('host')) - def test_instance_type_driver(self): - driver = host_filter.InstanceTypeFilter() + def test_instance_type_filter(self): + hf = host_filter.InstanceTypeFilter() # filter all hosts that can support 50 ram and 500 disk - name, cooked = driver.instance_type_to_filter(self.instance_type) + name, cooked = hf.instance_type_to_filter(self.instance_type) self.assertEquals('nova.scheduler.host_filter.InstanceTypeFilter', name) - hosts = driver.filter_hosts(self.zone_manager, cooked) + hosts = hf.filter_hosts(self.zone_manager, cooked) self.assertEquals(6, len(hosts)) just_hosts = [host for host, caps in hosts] just_hosts.sort() self.assertEquals('host05', just_hosts[0]) self.assertEquals('host10', just_hosts[5]) - def test_json_driver(self): - driver = host_filter.JsonFilter() + def test_json_filter(self): + hf = host_filter.JsonFilter() # filter all hosts that can support 50 ram and 500 disk - name, cooked = driver.instance_type_to_filter(self.instance_type) + name, cooked = hf.instance_type_to_filter(self.instance_type) self.assertEquals('nova.scheduler.host_filter.JsonFilter', name) - hosts = driver.filter_hosts(self.zone_manager, cooked) + hosts = hf.filter_hosts(self.zone_manager, cooked) self.assertEquals(6, len(hosts)) just_hosts = [host for host, caps in hosts] just_hosts.sort() @@ -141,7 +141,7 @@ class HostFilterTestCase(test.TestCase): ] ] cooked = json.dumps(raw) - hosts = driver.filter_hosts(self.zone_manager, cooked) + hosts = hf.filter_hosts(self.zone_manager, cooked) self.assertEquals(5, len(hosts)) just_hosts = [host for host, caps in hosts] @@ -153,7 +153,7 @@ class HostFilterTestCase(test.TestCase): ['=', '$compute.host_memory_free', 30], ] cooked = json.dumps(raw) - hosts = driver.filter_hosts(self.zone_manager, cooked) + hosts = hf.filter_hosts(self.zone_manager, cooked) self.assertEquals(9, len(hosts)) just_hosts = [host for host, caps in hosts] @@ -163,7 +163,7 @@ class HostFilterTestCase(test.TestCase): raw = ['in', '$compute.host_memory_free', 20, 40, 60, 80, 100] cooked = json.dumps(raw) - hosts = driver.filter_hosts(self.zone_manager, cooked) + hosts = hf.filter_hosts(self.zone_manager, cooked) self.assertEquals(5, len(hosts)) just_hosts = [host for host, caps in hosts] @@ -175,35 +175,32 @@ class HostFilterTestCase(test.TestCase): raw = ['unknown command', ] cooked = json.dumps(raw) try: - driver.filter_hosts(self.zone_manager, cooked) + hf.filter_hosts(self.zone_manager, cooked) self.fail("Should give KeyError") except KeyError, e: pass - self.assertTrue(driver.filter_hosts(self.zone_manager, json.dumps([]))) - self.assertTrue(driver.filter_hosts(self.zone_manager, json.dumps({}))) - self.assertTrue(driver.filter_hosts(self.zone_manager, json.dumps( + self.assertTrue(hf.filter_hosts(self.zone_manager, json.dumps([]))) + self.assertTrue(hf.filter_hosts(self.zone_manager, json.dumps({}))) + self.assertTrue(hf.filter_hosts(self.zone_manager, json.dumps( ['not', True, False, True, False] ))) try: - driver.filter_hosts(self.zone_manager, json.dumps( + hf.filter_hosts(self.zone_manager, json.dumps( 'not', True, False, True, False )) self.fail("Should give KeyError") except KeyError, e: pass - self.assertFalse(driver.filter_hosts(self.zone_manager, json.dumps( - ['=', '$foo', 100] - ))) - self.assertFalse(driver.filter_hosts(self.zone_manager, json.dumps( - ['=', '$.....', 100] - ))) - self.assertFalse(driver.filter_hosts(self.zone_manager, json.dumps( - ['>', ['and', ['or', ['not', ['<', ['>=', ['<=', ['in', ]]]]]]]] - ))) + self.assertFalse(hf.filter_hosts(self.zone_manager, + json.dumps(['=', '$foo', 100]))) + self.assertFalse(hf.filter_hosts(self.zone_manager, + json.dumps(['=', '$.....', 100]))) + self.assertFalse(hf.filter_hosts(self.zone_manager, + json.dumps( + ['>', ['and', ['or', ['not', ['<', ['>=', ['<=', ['in', ]]]]]]]]))) - self.assertFalse(driver.filter_hosts(self.zone_manager, json.dumps( - ['=', {}, ['>', '$missing....foo']] - ))) + self.assertFalse(hf.filter_hosts(self.zone_manager, + json.dumps(['=', {}, ['>', '$missing....foo']]))) -- cgit From f488576ae27f8eb96a04022d0ecd11a28bd15116 Mon Sep 17 00:00:00 2001 From: Alex Meade Date: Tue, 24 May 2011 16:44:28 -0400 Subject: Added filtering on image properties --- nova/api/openstack/images.py | 23 +++++++++++++++++++++-- nova/image/fake.py | 4 ++-- nova/image/glance.py | 8 ++++---- nova/tests/api/openstack/fakes.py | 4 ++-- nova/tests/api/openstack/test_images.py | 9 +++++++++ nova/tests/image/test_glance.py | 2 +- 6 files changed, 39 insertions(+), 11 deletions(-) (limited to 'nova') diff --git a/nova/api/openstack/images.py b/nova/api/openstack/images.py index 34d4c27fc..755ce8ead 100644 --- a/nova/api/openstack/images.py +++ b/nova/api/openstack/images.py @@ -28,6 +28,9 @@ from nova.api.openstack.views import images as images_view LOG = log.getLogger('nova.api.openstack.images') FLAGS = flags.FLAGS +SUPPORTED_FILTERS = ['name', 'status', 'container_format', 'disk_format', + 'size_min', 'size_max'] + class Controller(common.OpenstackController): """Base `wsgi.Controller` for retrieving/displaying images.""" @@ -59,7 +62,8 @@ class Controller(common.OpenstackController): :param req: `wsgi.Request` object """ context = req.environ['nova.context'] - images = self._image_service.index(context) + filters = self._get_filters(req) + images = self._image_service.index(context, filters) images = common.limited(images, req) builder = self.get_builder(req).build return dict(images=[builder(image, detail=False) for image in images]) @@ -70,11 +74,26 @@ class Controller(common.OpenstackController): :param req: `wsgi.Request` object. """ context = req.environ['nova.context'] - images = self._image_service.detail(context) + filters = self._get_filters(req) + images = self._image_service.detail(context, filters) images = common.limited(images, req) builder = self.get_builder(req).build return dict(images=[builder(image, detail=True) for image in images]) + def _get_filters(self, req): + """ + Return a dictionary of query param filters from the request + + :param req: the Request object coming from the wsgi layer + :retval a dict of key/value filters + """ + filters = {} + for param in req.str_params: + if param in SUPPORTED_FILTERS or param.startswith('property-'): + filters[param] = req.str_params.get(param) + + return filters + def show(self, req, id): """Return detailed information about a specific image. diff --git a/nova/image/fake.py b/nova/image/fake.py index b400b2adb..8e84c8597 100644 --- a/nova/image/fake.py +++ b/nova/image/fake.py @@ -52,11 +52,11 @@ class FakeImageService(service.BaseImageService): self.create(None, image) super(FakeImageService, self).__init__() - def index(self, context): + def index(self, context, filters=None): """Returns list of images.""" return copy.deepcopy(self.images.values()) - def detail(self, context): + def detail(self, context, filters=None): """Return list of detailed image information.""" return copy.deepcopy(self.images.values()) diff --git a/nova/image/glance.py b/nova/image/glance.py index 193e37273..dec797619 100644 --- a/nova/image/glance.py +++ b/nova/image/glance.py @@ -58,23 +58,23 @@ class GlanceImageService(service.BaseImageService): else: self.client = client - def index(self, context): + def index(self, context, filters=None): """Calls out to Glance for a list of images available.""" # NOTE(sirp): We need to use `get_images_detailed` and not # `get_images` here because we need `is_public` and `properties` # included so we can filter by user filtered = [] - image_metas = self.client.get_images_detailed() + image_metas = self.client.get_images_detailed(filters=filters) for image_meta in image_metas: if self._is_image_available(context, image_meta): meta_subset = utils.subset_dict(image_meta, ('id', 'name')) filtered.append(meta_subset) return filtered - def detail(self, context): + def detail(self, context, filters=None): """Calls out to Glance for a list of detailed image information.""" filtered = [] - image_metas = self.client.get_images_detailed() + image_metas = self.client.get_images_detailed(filters=filters) for image_meta in image_metas: if self._is_image_available(context, image_meta): base_image_meta = self._translate_to_base(image_meta) diff --git a/nova/tests/api/openstack/fakes.py b/nova/tests/api/openstack/fakes.py index bf51239e6..8e0156afa 100644 --- a/nova/tests/api/openstack/fakes.py +++ b/nova/tests/api/openstack/fakes.py @@ -166,11 +166,11 @@ def stub_out_glance(stubs, initial_fixtures=None): def __init__(self, initial_fixtures): self.fixtures = initial_fixtures or [] - def fake_get_images(self): + def fake_get_images(self, filters=None): return [dict(id=f['id'], name=f['name']) for f in self.fixtures] - def fake_get_images_detailed(self): + def fake_get_images_detailed(self, filters=None): return copy.deepcopy(self.fixtures) def fake_get_image_meta(self, image_id): diff --git a/nova/tests/api/openstack/test_images.py b/nova/tests/api/openstack/test_images.py index 2c329f920..76d4e2f56 100644 --- a/nova/tests/api/openstack/test_images.py +++ b/nova/tests/api/openstack/test_images.py @@ -708,6 +708,15 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): self.assertDictListMatch(expected, response_list) + def test_get_image_request_filters(self): + request =\ + webob.Request.blank('/v1.1/images/detail?status=ACTIVE&name=testname') + filters = images.Controller()._get_filters(request) + expected = {'status': 'ACTIVE', + 'name': 'testname', + } + self.assertDictMatch(expected, filters) + def test_get_image_found(self): req = webob.Request.blank('/v1.0/images/123') res = req.get_response(fakes.wsgi_app()) diff --git a/nova/tests/image/test_glance.py b/nova/tests/image/test_glance.py index 109905ded..6d108d494 100644 --- a/nova/tests/image/test_glance.py +++ b/nova/tests/image/test_glance.py @@ -34,7 +34,7 @@ class StubGlanceClient(object): def get_image_meta(self, image_id): return self.images[image_id] - def get_images_detailed(self): + def get_images_detailed(self, filters=None): return self.images.itervalues() def get_image(self, image_id): -- cgit From 17abaeafaf3fed2847e4377a16b47771eb663304 Mon Sep 17 00:00:00 2001 From: MORITA Kazutaka Date: Wed, 25 May 2011 16:27:28 +0900 Subject: Fix wrong call of the volume api create() --- nova/api/openstack/contrib/volumes.py | 2 +- nova/tests/test_quota.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) (limited to 'nova') diff --git a/nova/api/openstack/contrib/volumes.py b/nova/api/openstack/contrib/volumes.py index 18de2ec71..b22bd2846 100644 --- a/nova/api/openstack/contrib/volumes.py +++ b/nova/api/openstack/contrib/volumes.py @@ -135,7 +135,7 @@ class VolumeController(wsgi.Controller): vol = env['volume'] size = vol['size'] LOG.audit(_("Create volume of %s GB"), size, context=context) - new_volume = self.volume_api.create(context, size, + new_volume = self.volume_api.create(context, size, None, vol.get('display_name'), vol.get('display_description')) diff --git a/nova/tests/test_quota.py b/nova/tests/test_quota.py index 7ace2ad7d..990068fae 100644 --- a/nova/tests/test_quota.py +++ b/nova/tests/test_quota.py @@ -228,6 +228,7 @@ class QuotaTestCase(test.TestCase): volume.API().create, self.context, size=10, + snapshot_id=None, name='', description='') for volume_id in volume_ids: @@ -241,6 +242,7 @@ class QuotaTestCase(test.TestCase): volume.API().create, self.context, size=10, + snapshot_id=None, name='', description='') for volume_id in volume_ids: -- cgit From 7139cf1f0cfe9241a1710e5b7c621db569a2fc2d Mon Sep 17 00:00:00 2001 From: MORITA Kazutaka Date: Wed, 25 May 2011 16:37:52 +0900 Subject: Make snapshot_id=None a default value in VolumeManager:create_volume(). It is not a regular case to create a volume from a snapshot. --- nova/volume/manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'nova') diff --git a/nova/volume/manager.py b/nova/volume/manager.py index 84085fbd8..b6f0f5eeb 100644 --- a/nova/volume/manager.py +++ b/nova/volume/manager.py @@ -90,7 +90,7 @@ class VolumeManager(manager.SchedulerDependentManager): else: LOG.info(_("volume %s: skipping export"), volume['name']) - def create_volume(self, context, volume_id, snapshot_id): + def create_volume(self, context, volume_id, snapshot_id=None): """Creates and exports the volume.""" context = context.elevated() volume_ref = self.db.volume_get(context, volume_id) -- cgit From f3125b3012da7b6429e4e551060498e665c4596e Mon Sep 17 00:00:00 2001 From: MORITA Kazutaka Date: Wed, 25 May 2011 17:51:30 +0900 Subject: Add unittests for cloning volumes. --- nova/tests/test_cloud.py | 19 +++++++++++++++++++ nova/tests/test_volume.py | 20 +++++++++++++++++++- 2 files changed, 38 insertions(+), 1 deletion(-) (limited to 'nova') diff --git a/nova/tests/test_cloud.py b/nova/tests/test_cloud.py index d9169a646..8c7520fe8 100644 --- a/nova/tests/test_cloud.py +++ b/nova/tests/test_cloud.py @@ -171,6 +171,25 @@ class CloudTestCase(test.TestCase): db.volume_destroy(self.context, vol1['id']) db.volume_destroy(self.context, vol2['id']) + def test_create_volume_from_snapshot(self): + """Makes sure create_volume works when we specify a snapshot.""" + vol = db.volume_create(self.context, {'size': 1}) + 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') + + result = self.cloud.create_volume(self.context, + snapshot_id=snapshot_id) + volume_id = result['volumeId'] + result = self.cloud.describe_volumes(self.context) + self.assertEqual(len(result['volumeSet']), 2) + self.assertEqual(result['volumeSet'][1]['volumeId'], volume_id) + + db.volume_destroy(self.context, ec2utils.ec2_id_to_id(volume_id)) + db.snapshot_destroy(self.context, snap['id']) + db.volume_destroy(self.context, vol['id']) + def test_describe_availability_zones(self): """Makes sure describe_availability_zones works and filters results.""" service1 = db.service_create(self.context, {'host': 'host1_zones', diff --git a/nova/tests/test_volume.py b/nova/tests/test_volume.py index c66b66959..1c25d601a 100644 --- a/nova/tests/test_volume.py +++ b/nova/tests/test_volume.py @@ -45,10 +45,11 @@ class VolumeTestCase(test.TestCase): self.context = context.get_admin_context() @staticmethod - def _create_volume(size='0'): + def _create_volume(size='0', snapshot_id=None): """Create a volume object.""" vol = {} vol['size'] = size + vol['snapshot_id'] = snapshot_id vol['user_id'] = 'fake' vol['project_id'] = 'fake' vol['availability_zone'] = FLAGS.storage_availability_zone @@ -69,6 +70,23 @@ class VolumeTestCase(test.TestCase): self.context, volume_id) + def test_create_volume_from_snapshot(self): + """Test volume can be created from a snapshot.""" + volume_src_id = self._create_volume() + self.volume.create_volume(self.context, volume_src_id) + snapshot_id = self._create_snapshot(volume_src_id) + self.volume.create_snapshot(self.context, volume_src_id, snapshot_id) + volume_dst_id = self._create_volume(0, snapshot_id) + self.volume.create_volume(self.context, volume_dst_id, snapshot_id) + self.assertEqual(volume_dst_id, db.volume_get(context.get_admin_context(), + volume_dst_id).id) + self.assertEqual(snapshot_id, db.volume_get(context.get_admin_context(), + volume_dst_id).snapshot_id) + + self.volume.delete_volume(self.context, volume_dst_id) + self.volume.delete_snapshot(self.context, snapshot_id) + self.volume.delete_volume(self.context, volume_src_id) + def test_too_big_volume(self): """Ensure failure if a too large of a volume is requested.""" # FIXME(vish): validation needs to move into the data layer in -- cgit From d380729b162c8d6120279db74327e61a4942e28f Mon Sep 17 00:00:00 2001 From: MORITA Kazutaka Date: Wed, 25 May 2011 18:02:07 +0900 Subject: Avoid wildcard import. --- .../sqlalchemy/migrate_repo/versions/020_add_snapshot_id_to_volumes.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) (limited to 'nova') diff --git a/nova/db/sqlalchemy/migrate_repo/versions/020_add_snapshot_id_to_volumes.py b/nova/db/sqlalchemy/migrate_repo/versions/020_add_snapshot_id_to_volumes.py index 0a50123bf..10bd9d5c9 100644 --- a/nova/db/sqlalchemy/migrate_repo/versions/020_add_snapshot_id_to_volumes.py +++ b/nova/db/sqlalchemy/migrate_repo/versions/020_add_snapshot_id_to_volumes.py @@ -15,8 +15,7 @@ # License for the specific language governing permissions and limitations # under the License. -from sqlalchemy import * -from migrate import * +from sqlalchemy import Column, Table, MetaData, Integer from nova import log as logging -- cgit From 3d9569147cee2eaa94fc49c55b40f70a72171ebe Mon Sep 17 00:00:00 2001 From: Alex Meade Date: Wed, 25 May 2011 09:33:51 -0400 Subject: Added test --- nova/tests/api/openstack/test_images.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) (limited to 'nova') diff --git a/nova/tests/api/openstack/test_images.py b/nova/tests/api/openstack/test_images.py index 76d4e2f56..233419c6d 100644 --- a/nova/tests/api/openstack/test_images.py +++ b/nova/tests/api/openstack/test_images.py @@ -709,11 +709,20 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): self.assertDictListMatch(expected, response_list) def test_get_image_request_filters(self): - request =\ - webob.Request.blank('/v1.1/images/detail?status=ACTIVE&name=testname') + request = webob.Request.blank( + '/v1.1/images/detail?status=ACTIVE&name=testname&property-test=3') filters = images.Controller()._get_filters(request) expected = {'status': 'ACTIVE', 'name': 'testname', + 'property-test': '3', + } + self.assertDictMatch(expected, filters) + + def test_get_image_request_filters_not_supported(self): + request = webob.Request.blank( + '/v1.1/images/detail?status=ACTIVE&UNSUPPORTEDFILTER=testname') + filters = images.Controller()._get_filters(request) + expected = {'status': 'ACTIVE', } self.assertDictMatch(expected, filters) -- cgit From 537c5aea298a6c09b3329185c2d0eed77a0a21bd Mon Sep 17 00:00:00 2001 From: Mark Washenberger Date: Wed, 25 May 2011 12:09:53 -0400 Subject: try out mox for testing image request filters --- nova/tests/api/openstack/test_images.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) (limited to 'nova') diff --git a/nova/tests/api/openstack/test_images.py b/nova/tests/api/openstack/test_images.py index 233419c6d..e25334732 100644 --- a/nova/tests/api/openstack/test_images.py +++ b/nova/tests/api/openstack/test_images.py @@ -28,6 +28,7 @@ import shutil import tempfile import xml.dom.minidom as minidom +import mox import stubout import webob @@ -709,14 +710,20 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): self.assertDictListMatch(expected, response_list) def test_get_image_request_filters(self): + mocker = mox.Mox() + image_service = mocker.CreateMockAnything() + context = object() + filters = {'status': 'ACTIVE', + 'name': 'testname', + 'property-test': '3'} + image_service.detail(context, filters).AndReturn([]) + mocker.ReplayAll() request = webob.Request.blank( '/v1.1/images/detail?status=ACTIVE&name=testname&property-test=3') - filters = images.Controller()._get_filters(request) - expected = {'status': 'ACTIVE', - 'name': 'testname', - 'property-test': '3', - } - self.assertDictMatch(expected, filters) + request.environ['nova.context'] = context + controller = images.ControllerV11(image_service=image_service) + controller.detail(request) + mocker.VerifyAll() def test_get_image_request_filters_not_supported(self): request = webob.Request.blank( -- cgit From e4bf97ba29e8e5858f37cedb34e20ccd8e210bae Mon Sep 17 00:00:00 2001 From: Alex Meade Date: Wed, 25 May 2011 12:24:27 -0400 Subject: Updated tests to use mox pep8 --- nova/api/openstack/images.py | 2 +- nova/tests/api/openstack/test_images.py | 14 ++++++++++---- 2 files changed, 11 insertions(+), 5 deletions(-) (limited to 'nova') diff --git a/nova/api/openstack/images.py b/nova/api/openstack/images.py index 755ce8ead..553566d58 100644 --- a/nova/api/openstack/images.py +++ b/nova/api/openstack/images.py @@ -93,7 +93,7 @@ class Controller(common.OpenstackController): filters[param] = req.str_params.get(param) return filters - + def show(self, req, id): """Return detailed information about a specific image. diff --git a/nova/tests/api/openstack/test_images.py b/nova/tests/api/openstack/test_images.py index e25334732..f3f0217d6 100644 --- a/nova/tests/api/openstack/test_images.py +++ b/nova/tests/api/openstack/test_images.py @@ -726,12 +726,18 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): mocker.VerifyAll() def test_get_image_request_filters_not_supported(self): + mocker = mox.Mox() + image_service = mocker.CreateMockAnything() + context = object() + filters = {'status': 'ACTIVE'} + image_service.detail(context, filters).AndReturn([]) + mocker.ReplayAll() request = webob.Request.blank( '/v1.1/images/detail?status=ACTIVE&UNSUPPORTEDFILTER=testname') - filters = images.Controller()._get_filters(request) - expected = {'status': 'ACTIVE', - } - self.assertDictMatch(expected, filters) + request.environ['nova.context'] = context + controller = images.ControllerV11(image_service=image_service) + controller.detail(request) + mocker.VerifyAll() def test_get_image_found(self): req = webob.Request.blank('/v1.0/images/123') -- cgit From c440aecaaacf3caa8683234022bc10836d232971 Mon Sep 17 00:00:00 2001 From: Alex Meade Date: Wed, 25 May 2011 17:28:10 -0400 Subject: Added params to local and base image service --- nova/image/local.py | 4 ++-- nova/image/service.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) (limited to 'nova') diff --git a/nova/image/local.py b/nova/image/local.py index 918180bae..677d5302b 100644 --- a/nova/image/local.py +++ b/nova/image/local.py @@ -63,7 +63,7 @@ class LocalImageService(service.BaseImageService): images.append(unhexed_image_id) return images - def index(self, context): + def index(self, context, *args, **kwargs): filtered = [] image_metas = self.detail(context) for image_meta in image_metas: @@ -71,7 +71,7 @@ class LocalImageService(service.BaseImageService): filtered.append(meta) return filtered - def detail(self, context): + def detail(self, context, *args, **kwargs): images = [] for image_id in self._ids(): try: diff --git a/nova/image/service.py b/nova/image/service.py index ab6749049..5361cfc89 100644 --- a/nova/image/service.py +++ b/nova/image/service.py @@ -46,7 +46,7 @@ class BaseImageService(object): # the ImageService subclass SERVICE_IMAGE_ATTRS = [] - def index(self, context): + def index(self, context, *args, **kwargs): """List images. :returns: a sequence of mappings with the following signature @@ -55,7 +55,7 @@ class BaseImageService(object): """ raise NotImplementedError - def detail(self, context): + def detail(self, context, *args, **kwargs): """Detailed information about an images. :returns: a sequence of mappings with the following signature -- cgit From 34d660128b8fa935dc1a1d5b0b22e8177135d3a8 Mon Sep 17 00:00:00 2001 From: Kei Masumoto Date: Thu, 26 May 2011 19:46:11 +0900 Subject: creating _take_action_to_instance to nova.virt.libvirt_conn.py --- nova/virt/libvirt_conn.py | 43 +++++++++++++++---------------------------- 1 file changed, 15 insertions(+), 28 deletions(-) (limited to 'nova') diff --git a/nova/virt/libvirt_conn.py b/nova/virt/libvirt_conn.py index aa5e42fc8..4e2e2292e 100644 --- a/nova/virt/libvirt_conn.py +++ b/nova/virt/libvirt_conn.py @@ -45,6 +45,7 @@ import sys import tempfile import time import uuid +import inspect from xml.dom import minidom from xml.etree import ElementTree @@ -548,53 +549,39 @@ class LibvirtConnection(driver.ComputeDriver): timer = utils.LoopingCall(_wait_for_reboot) return timer.start(interval=0.5, now=True) - @exception.wrap_exception - def pause(self, instance, callback): - """Pause VM instance""" + def _take_action_to_instance(self, action, instance, *arg): + """action VM instance""" if self.read_only: tmpconn = self._connect(self.libvirt_uri, False) dom = tmpconn.lookupByName(instance.name) - dom.suspend() + method = getattr(dom, action) + method(*arg) tmpconn.close() else: dom = self._conn.lookupByName(instance.name) - dom.suspend() + method = getattr(dom, action) + method(*arg) + + @exception.wrap_exception + def pause(self, instance, callback): + """Pause VM instance""" + self._take_action_to_instance("suspend", instance) @exception.wrap_exception def unpause(self, instance, callback): """Unpause paused VM instance""" - if self.read_only: - tmpconn = self._connect(self.libvirt_uri, False) - dom = tmpconn.lookupByName(instance.name) - dom.resume() - tmpconn.close() - else: - dom = self._conn.lookupByName(instance.name) - dom.resume() + self._take_action_to_instance("resume", instance) @exception.wrap_exception def suspend(self, instance, callback): """Suspend the specified instance""" - if self.read_only: - tmpconn = self._connect(self.libvirt_uri, False) - dom = tmpconn.lookupByName(instance.name) - dom.managedSave(0) - tmpconn.close() - else: - dom = self._conn.lookupByName(instance.name) - dom.managedSave(0) + self._take_action_to_instance("managedSave", instance, 0) @exception.wrap_exception def resume(self, instance, callback): """resume the specified instance""" try: - if self.read_only: - tmpconn = self._connect(self.libvirt_uri, False) - dom = tmpconn.lookupByName(instance.name) - tmpconn.close() - else: - dom = self._conn.lookupByName(instance.name) - dom.create() + self._take_action_to_instance("create", instance) except libvirt.LibvirtError: xml = self.to_xml(instance, None) self._create_new_domain(xml) -- cgit From ce5c7287e06cb7ce1d1a41354a5d6ea073d308d0 Mon Sep 17 00:00:00 2001 From: Kei Masumoto Date: Thu, 26 May 2011 20:31:50 +0900 Subject: remove unnecessary import inspect at nova.virt.libvirt_conn --- nova/virt/libvirt_conn.py | 1 - 1 file changed, 1 deletion(-) (limited to 'nova') diff --git a/nova/virt/libvirt_conn.py b/nova/virt/libvirt_conn.py index 4e2e2292e..f9c441505 100644 --- a/nova/virt/libvirt_conn.py +++ b/nova/virt/libvirt_conn.py @@ -45,7 +45,6 @@ import sys import tempfile import time import uuid -import inspect from xml.dom import minidom from xml.etree import ElementTree -- cgit From 87717c33ae78201a24c0f5a3416ae4b0080e4668 Mon Sep 17 00:00:00 2001 From: Kei Masumoto Date: Thu, 26 May 2011 20:49:14 +0900 Subject: replace double quatation to single quatation at nova.virt.libvirt_conn --- nova/virt/libvirt_conn.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) (limited to 'nova') diff --git a/nova/virt/libvirt_conn.py b/nova/virt/libvirt_conn.py index f9c441505..8c9a3550a 100644 --- a/nova/virt/libvirt_conn.py +++ b/nova/virt/libvirt_conn.py @@ -564,23 +564,23 @@ class LibvirtConnection(driver.ComputeDriver): @exception.wrap_exception def pause(self, instance, callback): """Pause VM instance""" - self._take_action_to_instance("suspend", instance) + self._take_action_to_instance('suspend', instance) @exception.wrap_exception def unpause(self, instance, callback): """Unpause paused VM instance""" - self._take_action_to_instance("resume", instance) + self._take_action_to_instance('resume', instance) @exception.wrap_exception def suspend(self, instance, callback): """Suspend the specified instance""" - self._take_action_to_instance("managedSave", instance, 0) + self._take_action_to_instance('managedSave', instance, 0) @exception.wrap_exception def resume(self, instance, callback): """resume the specified instance""" try: - self._take_action_to_instance("create", instance) + self._take_action_to_instance('create', instance) except libvirt.LibvirtError: xml = self.to_xml(instance, None) self._create_new_domain(xml) -- cgit From f37d94428dd0b56632958d5d3a6930531a51cd44 Mon Sep 17 00:00:00 2001 From: Alex Meade Date: Thu, 26 May 2011 10:54:46 -0400 Subject: Restricted image filtering by name and status only --- nova/api/openstack/images.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) (limited to 'nova') diff --git a/nova/api/openstack/images.py b/nova/api/openstack/images.py index 553566d58..2e779da79 100644 --- a/nova/api/openstack/images.py +++ b/nova/api/openstack/images.py @@ -28,8 +28,7 @@ from nova.api.openstack.views import images as images_view LOG = log.getLogger('nova.api.openstack.images') FLAGS = flags.FLAGS -SUPPORTED_FILTERS = ['name', 'status', 'container_format', 'disk_format', - 'size_min', 'size_max'] +SUPPORTED_FILTERS = ['name', 'status'] class Controller(common.OpenstackController): -- cgit From ff75e808eef06a72c0198fe976c19c60256c6b74 Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Thu, 26 May 2011 10:28:22 -0700 Subject: log upload errors --- nova/image/s3.py | 11 +++++++++++ 1 file changed, 11 insertions(+) (limited to 'nova') diff --git a/nova/image/s3.py b/nova/image/s3.py index 673cbf56f..bad04e5c0 100644 --- a/nova/image/s3.py +++ b/nova/image/s3.py @@ -31,12 +31,14 @@ import eventlet from nova import crypto from nova import exception from nova import flags +from nova import log as logging from nova import utils from nova.auth import manager from nova.image import service from nova.api.ec2 import ec2utils +LOG = logging.getLogger("nova.image.s3") FLAGS = flags.FLAGS flags.DEFINE_string('image_decryption_dir', '/tmp', 'parent dir for tempdir used for image decryption') @@ -181,6 +183,8 @@ class S3ImageService(service.BaseImageService): shutil.copyfileobj(part, combined) except Exception: + LOG.exception(_("Failed to download %(image_location)s " + "to %(image_path)s"), locals()) metadata['properties']['image_state'] = 'failed_download' self.service.update(context, image_id, metadata) raise @@ -203,6 +207,9 @@ class S3ImageService(service.BaseImageService): encrypted_iv, cloud_pk, dec_filename) except Exception: + LOG.exception(_("Failed to decrypt %(image_location)s " + "to %(image_path)s"), locals()) + LOG.exception(_("Failed to decrypt %s"), enc_filename) metadata['properties']['image_state'] = 'failed_decrypt' self.service.update(context, image_id, metadata) raise @@ -213,6 +220,8 @@ class S3ImageService(service.BaseImageService): try: unz_filename = self._untarzip_image(image_path, dec_filename) except Exception: + LOG.exception(_("Failed to untar %(image_location)s " + "to %(image_path)s"), locals()) metadata['properties']['image_state'] = 'failed_untar' self.service.update(context, image_id, metadata) raise @@ -224,6 +233,8 @@ class S3ImageService(service.BaseImageService): self.service.update(context, image_id, metadata, image_file) except Exception: + LOG.exception(_("Failed to upload %(image_location)s " + "to %(image_path)s"), locals()) metadata['properties']['image_state'] = 'failed_upload' self.service.update(context, image_id, metadata) raise -- cgit From 75ec3d77d3fa4078bbe7d647377f987d87d97651 Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Thu, 26 May 2011 10:30:27 -0700 Subject: exceptions are logged via the raise, so just log an error message --- nova/image/s3.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) (limited to 'nova') diff --git a/nova/image/s3.py b/nova/image/s3.py index bad04e5c0..ec8852f09 100644 --- a/nova/image/s3.py +++ b/nova/image/s3.py @@ -183,8 +183,8 @@ class S3ImageService(service.BaseImageService): shutil.copyfileobj(part, combined) except Exception: - LOG.exception(_("Failed to download %(image_location)s " - "to %(image_path)s"), locals()) + LOG.error(_("Failed to download %(image_location)s " + "to %(image_path)s"), locals()) metadata['properties']['image_state'] = 'failed_download' self.service.update(context, image_id, metadata) raise @@ -207,9 +207,8 @@ class S3ImageService(service.BaseImageService): encrypted_iv, cloud_pk, dec_filename) except Exception: - LOG.exception(_("Failed to decrypt %(image_location)s " - "to %(image_path)s"), locals()) - LOG.exception(_("Failed to decrypt %s"), enc_filename) + LOG.error(_("Failed to decrypt %(image_location)s " + "to %(image_path)s"), locals()) metadata['properties']['image_state'] = 'failed_decrypt' self.service.update(context, image_id, metadata) raise @@ -220,8 +219,8 @@ class S3ImageService(service.BaseImageService): try: unz_filename = self._untarzip_image(image_path, dec_filename) except Exception: - LOG.exception(_("Failed to untar %(image_location)s " - "to %(image_path)s"), locals()) + LOG.error(_("Failed to untar %(image_location)s " + "to %(image_path)s"), locals()) metadata['properties']['image_state'] = 'failed_untar' self.service.update(context, image_id, metadata) raise @@ -233,8 +232,8 @@ class S3ImageService(service.BaseImageService): self.service.update(context, image_id, metadata, image_file) except Exception: - LOG.exception(_("Failed to upload %(image_location)s " - "to %(image_path)s"), locals()) + LOG.error(_("Failed to upload %(image_location)s " + "to %(image_path)s"), locals()) metadata['properties']['image_state'] = 'failed_upload' self.service.update(context, image_id, metadata) raise -- cgit From 788893ef2a17a4fda5e907f048a94f3ed0435bbc Mon Sep 17 00:00:00 2001 From: Sandy Walsh Date: Thu, 26 May 2011 10:53:48 -0700 Subject: missed a driver reference --- nova/compute/api.py | 2 +- nova/scheduler/host_filter.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) (limited to 'nova') diff --git a/nova/compute/api.py b/nova/compute/api.py index 216550e40..0b2c4e642 100644 --- a/nova/compute/api.py +++ b/nova/compute/api.py @@ -264,7 +264,7 @@ class API(base.Base): "instance_id": instance_id, "request_spec": { 'instance_type': instance_type, - 'filter_driver': + 'filter': 'nova.scheduler.host_filter.' 'InstanceTypeFilter' }, diff --git a/nova/scheduler/host_filter.py b/nova/scheduler/host_filter.py index d9771754a..ed76c90bf 100644 --- a/nova/scheduler/host_filter.py +++ b/nova/scheduler/host_filter.py @@ -296,13 +296,13 @@ class HostFilterScheduler(zone_aware_scheduler.ZoneAwareScheduler): hosts for weighing. The particular filter used may be passed in as an argument or the default will be used. - request_spec = {'filter_name': , + request_spec = {'filter': , 'instance_type': } """ def filter_hosts(self, num, request_spec): """Filter the full host list (from the ZoneManager)""" - filter_name = request_spec.get('filter_name', None) + filter_name = request_spec.get('filter', None) host_filter = choose_host_filter(filter_name) # TODO(sandy): We're only using InstanceType-based specs -- cgit From b9b16ca71d4bbb9782482bdf5d848bb5b787732f Mon Sep 17 00:00:00 2001 From: Alex Meade Date: Thu, 26 May 2011 13:59:25 -0400 Subject: Expanded tests --- nova/tests/api/openstack/test_images.py | 122 ++++++++++++++++++++++++++++++-- 1 file changed, 116 insertions(+), 6 deletions(-) (limited to 'nova') diff --git a/nova/tests/api/openstack/test_images.py b/nova/tests/api/openstack/test_images.py index f3f0217d6..9f1f28611 100644 --- a/nova/tests/api/openstack/test_images.py +++ b/nova/tests/api/openstack/test_images.py @@ -709,23 +709,119 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): self.assertDictListMatch(expected, response_list) - def test_get_image_request_filters(self): + def test_image_filter_with_name(self): mocker = mox.Mox() image_service = mocker.CreateMockAnything() context = object() - filters = {'status': 'ACTIVE', - 'name': 'testname', - 'property-test': '3'} + filters = {'name': 'testname'} + image_service.index(context, filters).AndReturn([]) + mocker.ReplayAll() + request = webob.Request.blank( + '/v1.1/images?name=testname') + request.environ['nova.context'] = context + controller = images.ControllerV11(image_service=image_service) + controller.index(request) + mocker.VerifyAll() + + def test_image_filter_with_status(self): + mocker = mox.Mox() + image_service = mocker.CreateMockAnything() + context = object() + filters = {'status': 'ACTIVE'} + image_service.index(context, filters).AndReturn([]) + mocker.ReplayAll() + request = webob.Request.blank( + '/v1.1/images?status=ACTIVE') + request.environ['nova.context'] = context + controller = images.ControllerV11(image_service=image_service) + controller.index(request) + mocker.VerifyAll() + + def test_image_filter_with_property(self): + mocker = mox.Mox() + image_service = mocker.CreateMockAnything() + context = object() + filters = {'property-test': '3'} + image_service.index(context, filters).AndReturn([]) + mocker.ReplayAll() + request = webob.Request.blank( + '/v1.1/images?property-test=3') + request.environ['nova.context'] = context + controller = images.ControllerV11(image_service=image_service) + controller.index(request) + mocker.VerifyAll() + + def test_image_filter_not_supported(self): + mocker = mox.Mox() + image_service = mocker.CreateMockAnything() + context = object() + filters = {'status': 'ACTIVE'} + image_service.index(context, filters).AndReturn([]) + mocker.ReplayAll() + request = webob.Request.blank( + '/v1.1/images?status=ACTIVE&UNSUPPORTEDFILTER=testname') + request.environ['nova.context'] = context + controller = images.ControllerV11(image_service=image_service) + controller.index(request) + mocker.VerifyAll() + + def test_image_no_filters(self): + mocker = mox.Mox() + image_service = mocker.CreateMockAnything() + context = object() + filters = {} + image_service.index(context, filters).AndReturn([]) + mocker.ReplayAll() + request = webob.Request.blank( + '/v1.1/images') + request.environ['nova.context'] = context + controller = images.ControllerV11(image_service=image_service) + controller.index(request) + mocker.VerifyAll() + + def test_image_detail_filter_with_name(self): + mocker = mox.Mox() + image_service = mocker.CreateMockAnything() + context = object() + filters = {'name': 'testname'} + image_service.detail(context, filters).AndReturn([]) + mocker.ReplayAll() + request = webob.Request.blank( + '/v1.1/images/detail?name=testname') + request.environ['nova.context'] = context + controller = images.ControllerV11(image_service=image_service) + controller.detail(request) + mocker.VerifyAll() + + def test_image_detail_filter_with_status(self): + mocker = mox.Mox() + image_service = mocker.CreateMockAnything() + context = object() + filters = {'status': 'ACTIVE'} + image_service.detail(context, filters).AndReturn([]) + mocker.ReplayAll() + request = webob.Request.blank( + '/v1.1/images/detail?status=ACTIVE') + request.environ['nova.context'] = context + controller = images.ControllerV11(image_service=image_service) + controller.detail(request) + mocker.VerifyAll() + + def test_image_detail_filter_with_property(self): + mocker = mox.Mox() + image_service = mocker.CreateMockAnything() + context = object() + filters = {'property-test': '3'} image_service.detail(context, filters).AndReturn([]) mocker.ReplayAll() request = webob.Request.blank( - '/v1.1/images/detail?status=ACTIVE&name=testname&property-test=3') + '/v1.1/images/detail?property-test=3') request.environ['nova.context'] = context controller = images.ControllerV11(image_service=image_service) controller.detail(request) mocker.VerifyAll() - def test_get_image_request_filters_not_supported(self): + def test_image_detail_filter_not_supported(self): mocker = mox.Mox() image_service = mocker.CreateMockAnything() context = object() @@ -739,6 +835,20 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): controller.detail(request) mocker.VerifyAll() + def test_image_detail_no_filters(self): + mocker = mox.Mox() + image_service = mocker.CreateMockAnything() + context = object() + filters = {} + image_service.detail(context, filters).AndReturn([]) + mocker.ReplayAll() + request = webob.Request.blank( + '/v1.1/images/detail') + request.environ['nova.context'] = context + controller = images.ControllerV11(image_service=image_service) + controller.detail(request) + mocker.VerifyAll() + def test_get_image_found(self): req = webob.Request.blank('/v1.0/images/123') res = req.get_response(fakes.wsgi_app()) -- cgit From c229d6e32f5275b2eb10e760f89a52dc31635c47 Mon Sep 17 00:00:00 2001 From: MORITA Kazutaka Date: Fri, 27 May 2011 14:13:17 +0900 Subject: Fix pep8 errors. --- nova/api/ec2/cloud.py | 7 ++++--- nova/tests/test_volume.py | 10 ++++++---- nova/volume/api.py | 3 ++- nova/volume/driver.py | 4 ++-- nova/volume/manager.py | 5 +++-- 5 files changed, 17 insertions(+), 12 deletions(-) (limited to 'nova') diff --git a/nova/api/ec2/cloud.py b/nova/api/ec2/cloud.py index b717a10c0..79cc3b3bf 100644 --- a/nova/api/ec2/cloud.py +++ b/nova/api/ec2/cloud.py @@ -666,14 +666,15 @@ class CloudController(object): return v def create_volume(self, context, **kwargs): - size = kwargs.get('size'); + size = kwargs.get('size') if kwargs.get('snapshot_id') != None: snapshot_id = ec2utils.ec2_id_to_id(kwargs['snapshot_id']) - LOG.audit(_("Create volume from snapshot %s"), snapshot_id, context=context) + LOG.audit(_("Create volume from snapshot %s"), snapshot_id, + context=context) else: snapshot_id = None LOG.audit(_("Create volume of %s GB"), size, context=context) - + volume = self.volume_api.create( context, size=size, diff --git a/nova/tests/test_volume.py b/nova/tests/test_volume.py index 8d58b3135..4f10ee6af 100644 --- a/nova/tests/test_volume.py +++ b/nova/tests/test_volume.py @@ -78,10 +78,12 @@ class VolumeTestCase(test.TestCase): self.volume.create_snapshot(self.context, volume_src_id, snapshot_id) volume_dst_id = self._create_volume(0, snapshot_id) self.volume.create_volume(self.context, volume_dst_id, snapshot_id) - self.assertEqual(volume_dst_id, db.volume_get(context.get_admin_context(), - volume_dst_id).id) - self.assertEqual(snapshot_id, db.volume_get(context.get_admin_context(), - volume_dst_id).snapshot_id) + self.assertEqual(volume_dst_id, db.volume_get( + context.get_admin_context(), + volume_dst_id).id) + self.assertEqual(snapshot_id, db.volume_get( + context.get_admin_context(), + volume_dst_id).snapshot_id) self.volume.delete_volume(self.context, volume_dst_id) self.volume.delete_snapshot(self.context, snapshot_id) diff --git a/nova/volume/api.py b/nova/volume/api.py index 7fa80383b..5804955f7 100644 --- a/nova/volume/api.py +++ b/nova/volume/api.py @@ -43,7 +43,8 @@ class API(base.Base): if snapshot_id != None: snapshot = self.get_snapshot(context, snapshot_id) if snapshot['status'] != "available": - raise exception.ApiError(_("Snapshot status must be available")) + raise exception.ApiError( + _("Snapshot status must be available")) size = snapshot['volume_size'] if quota.allowed_volumes(context, 1, size) < 1: diff --git a/nova/volume/driver.py b/nova/volume/driver.py index df9767a79..87e13277f 100644 --- a/nova/volume/driver.py +++ b/nova/volume/driver.py @@ -674,10 +674,10 @@ class SheepdogDriver(VolumeDriver): def create_volume_from_snapshot(self, volume, snapshot): """Creates a sheepdog volume from a snapshot.""" self._try_execute('qemu-img', 'create', '-b', - "sheepdog:%s:%s" % (snapshot['volume_name'], snapshot['name']), + "sheepdog:%s:%s" % (snapshot['volume_name'], + snapshot['name']), "sheepdog:%s" % volume['name']) - def delete_volume(self, volume): """Deletes a logical volume""" self._try_execute('collie', 'vdi', 'delete', volume['name']) diff --git a/nova/volume/manager.py b/nova/volume/manager.py index 673771aa7..ff53f0701 100644 --- a/nova/volume/manager.py +++ b/nova/volume/manager.py @@ -112,8 +112,9 @@ class VolumeManager(manager.SchedulerDependentManager): model_update = self.driver.create_volume(volume_ref) else: snapshot_ref = self.db.snapshot_get(context, snapshot_id) - model_update = self.driver.create_volume_from_snapshot(volume_ref, - snapshot_ref) + model_update = self.driver.create_volume_from_snapshot( + volume_ref, + snapshot_ref) if model_update: self.db.volume_update(context, volume_ref['id'], model_update) -- cgit From ca5a91b3fe6eaa1c2d2b85cb5a11d2bb36e7a436 Mon Sep 17 00:00:00 2001 From: Kei Masumoto Date: Fri, 27 May 2011 15:14:16 +0900 Subject: fixed read_only check --- nova/virt/libvirt_conn.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) (limited to 'nova') diff --git a/nova/virt/libvirt_conn.py b/nova/virt/libvirt_conn.py index 8c9a3550a..7982611fa 100644 --- a/nova/virt/libvirt_conn.py +++ b/nova/virt/libvirt_conn.py @@ -564,23 +564,27 @@ class LibvirtConnection(driver.ComputeDriver): @exception.wrap_exception def pause(self, instance, callback): """Pause VM instance""" - self._take_action_to_instance('suspend', instance) + dom = self._lookup_by_name(instance.name) + dom.suspend() @exception.wrap_exception def unpause(self, instance, callback): """Unpause paused VM instance""" - self._take_action_to_instance('resume', instance) + dom = self._lookup_by_name(instance.name) + dom.resume() @exception.wrap_exception def suspend(self, instance, callback): """Suspend the specified instance""" - self._take_action_to_instance('managedSave', instance, 0) + dom = self._lookup_by_name(instance.name) + dom.managedSave(0) @exception.wrap_exception def resume(self, instance, callback): """resume the specified instance""" try: - self._take_action_to_instance('create', instance) + dom = self._lookup_by_name(instance.name) + dom.create() except libvirt.LibvirtError: xml = self.to_xml(instance, None) self._create_new_domain(xml) -- cgit From 34bd57c380c348fa9c60cf6b3371352da6e8853c Mon Sep 17 00:00:00 2001 From: Kei Masumoto Date: Fri, 27 May 2011 16:03:56 +0900 Subject: remove _take_action_to_instance --- nova/virt/libvirt_conn.py | 13 ------------- 1 file changed, 13 deletions(-) (limited to 'nova') diff --git a/nova/virt/libvirt_conn.py b/nova/virt/libvirt_conn.py index 7982611fa..47a77b3ae 100644 --- a/nova/virt/libvirt_conn.py +++ b/nova/virt/libvirt_conn.py @@ -548,19 +548,6 @@ class LibvirtConnection(driver.ComputeDriver): timer = utils.LoopingCall(_wait_for_reboot) return timer.start(interval=0.5, now=True) - def _take_action_to_instance(self, action, instance, *arg): - """action VM instance""" - if self.read_only: - tmpconn = self._connect(self.libvirt_uri, False) - dom = tmpconn.lookupByName(instance.name) - method = getattr(dom, action) - method(*arg) - tmpconn.close() - else: - dom = self._conn.lookupByName(instance.name) - method = getattr(dom, action) - method(*arg) - @exception.wrap_exception def pause(self, instance, callback): """Pause VM instance""" -- cgit From bd19bd2edd612dfea09e4230c59422e59c6de181 Mon Sep 17 00:00:00 2001 From: Sandy Walsh Date: Fri, 27 May 2011 05:01:42 -0700 Subject: fixed docstrings and general tidying --- nova/compute/api.py | 39 ++++++++++++++++---------------- nova/scheduler/host_filter.py | 41 +++++++++++++++++++++------------- nova/scheduler/zone_aware_scheduler.py | 33 +++++++++++++++++---------- 3 files changed, 67 insertions(+), 46 deletions(-) (limited to 'nova') diff --git a/nova/compute/api.py b/nova/compute/api.py index 7f1fe1b5c..3e5105050 100644 --- a/nova/compute/api.py +++ b/nova/compute/api.py @@ -88,10 +88,10 @@ class API(base.Base): {"method": "get_network_topic", "args": {'fake': 1}}) def _check_injected_file_quota(self, context, injected_files): - """Enforce quota limits on injected files. + """ + Enforce quota limits on injected files. Raises a QuotaError if any limit is exceeded. - """ if injected_files is None: return @@ -137,10 +137,10 @@ class API(base.Base): availability_zone=None, user_data=None, metadata={}, injected_files=None, admin_password=None): - """Create the number and type of instances requested. + """ + Create the number and type of instances requested. Verifies that quota and other arguments are valid. - """ if not instance_type: instance_type = instance_types.get_default_instance_type() @@ -293,13 +293,13 @@ class API(base.Base): return False def ensure_default_security_group(self, context): - """Ensure that a context has a security group. + """ + Ensure that a context has a security group. Creates a security group for the security context if it does not already exist. :param context: the security context - """ try: db.security_group_get_by_name(context, context.project_id, @@ -328,11 +328,11 @@ class API(base.Base): "args": {"security_group_id": security_group.id}}) def trigger_security_group_members_refresh(self, context, group_id): - """Called when a security group gains a new or loses a member. + """ + Called when a security group gains a new or loses a member. Sends an update request to each compute node for whom this is relevant. - """ # First, we get the security group rules that reference this group as # the grantee.. @@ -370,7 +370,8 @@ class API(base.Base): "args": {"security_group_id": group_id}}) def update(self, context, instance_id, **kwargs): - """Updates the instance in the datastore. + """ + Updates the instance in the datastore. :param context: The security context :param instance_id: ID of the instance to update @@ -379,7 +380,6 @@ class API(base.Base): updated :returns: None - """ rv = self.db.instance_update(context, instance_id, kwargs) return dict(rv.iteritems()) @@ -424,22 +424,22 @@ class API(base.Base): @scheduler_api.reroute_compute("get") def routing_get(self, context, instance_id): - """A version of get with special routing characteristics. + """ + A version of get with special routing characteristics. Use this method instead of get() if this is the only operation you intend to to. It will route to novaclient.get if the instance is not found. - """ return self.get(context, instance_id) def get_all(self, context, project_id=None, reservation_id=None, fixed_ip=None): - """Get all instances filtered by one of the given parameters. + """ + Get all instances filtered by one of the given parameters. If there is no filter and the context is an admin, it will retreive all instances in the system. - """ if reservation_id is not None: return self.db.instance_get_all_by_reservation( @@ -463,13 +463,13 @@ class API(base.Base): def _cast_compute_message(self, method, context, instance_id, host=None, params=None): - """Generic handler for RPC casts to compute. + """ + Generic handler for RPC casts to compute. :param params: Optional dictionary of arguments to be passed to the compute worker :returns: None - """ if not params: params = {} @@ -483,7 +483,8 @@ class API(base.Base): def _call_compute_message(self, method, context, instance_id, host=None, params=None): - """Generic handler for RPC calls to compute. + """ + Generic handler for RPC calls to compute. :param params: Optional dictionary of arguments to be passed to the compute worker @@ -516,10 +517,10 @@ class API(base.Base): % instance_id) def snapshot(self, context, instance_id, name): - """Snapshot the given instance. + """ + Snapshot the given instance. :returns: A dict containing image metadata - """ properties = {'instance_id': str(instance_id), 'user_id': str(context.user_id)} diff --git a/nova/scheduler/host_filter.py b/nova/scheduler/host_filter.py index ed76c90bf..89faace45 100644 --- a/nova/scheduler/host_filter.py +++ b/nova/scheduler/host_filter.py @@ -69,9 +69,11 @@ class HostFilter(object): class AllHostsFilter(HostFilter): - """NOP host filter. Returns all hosts in ZoneManager. + """ + NOP host filter. Returns all hosts in ZoneManager. This essentially does what the old Scheduler+Chance used - to give us.""" + to give us. + """ def instance_type_to_filter(self, instance_type): """Return anything to prevent base-class from raising @@ -133,8 +135,10 @@ class InstanceTypeFilter(HostFilter): class JsonFilter(HostFilter): - """Host Filter to allow simple JSON-based grammar for - selecting hosts.""" + """ + Host Filter to allow simple JSON-based grammar for + selecting hosts. + """ def _equals(self, args): """First term is == all the other terms.""" @@ -229,8 +233,10 @@ class JsonFilter(HostFilter): return (self._full_name(), json.dumps(query)) def _parse_string(self, string, host, services): - """Strings prefixed with $ are capability lookups in the - form '$service.capability[.subcap*]'""" + """ + Strings prefixed with $ are capability lookups in the + form '$service.capability[.subcap*]' + """ if not string: return None if string[0] != '$': @@ -277,22 +283,25 @@ FILTERS = [AllHostsFilter, InstanceTypeFilter, JsonFilter] def choose_host_filter(filter_name=None): - """Since the caller may specify which filter to use we need - to have an authoritative list of what is permissible. This - function checks the filter name against a predefined set - of acceptable filters.""" + """ + Since the caller may specify which filter to use we need + to have an authoritative list of what is permissible. This + function checks the filter name against a predefined set + of acceptable filters. + """ if not filter_name: filter_name = FLAGS.default_host_filter for filter_class in FILTERS: - if "%s.%s" % (filter_class.__module__, filter_class.__name__) == \ - filter_name: + host_match = "%s.%s" % (filter_class.__module__, filter_class.__name__) + if host_match == filter_name: return filter_class() raise exception.SchedulerHostFilterNotFound(filter_name=filter_name) class HostFilterScheduler(zone_aware_scheduler.ZoneAwareScheduler): - """The HostFilterScheduler uses the HostFilter to filter + """ + The HostFilterScheduler uses the HostFilter to filter hosts for weighing. The particular filter used may be passed in as an argument or the default will be used. @@ -313,6 +322,8 @@ class HostFilterScheduler(zone_aware_scheduler.ZoneAwareScheduler): return host_filter.filter_hosts(self.zone_manager, query) def weigh_hosts(self, num, request_spec, hosts): - """Derived classes must override this method and return - a lists of hosts in [{weight, hostname}] format.""" + """ + Derived classes must override this method and return + a lists of hosts in [{weight, hostname}] format. + """ return [dict(weight=1, hostname=host) for host, caps in hosts] diff --git a/nova/scheduler/zone_aware_scheduler.py b/nova/scheduler/zone_aware_scheduler.py index dc18fc427..236907626 100644 --- a/nova/scheduler/zone_aware_scheduler.py +++ b/nova/scheduler/zone_aware_scheduler.py @@ -40,13 +40,15 @@ class ZoneAwareScheduler(driver.Scheduler): def schedule_run_instance(self, context, instance_id, request_spec, *args, **kwargs): - """This method is called from nova.compute.api to provision + """ + This method is called from nova.compute.api to provision an instance. However we need to look at the parameters being passed in to see if this is a request to: 1. Create a Build Plan and then provision, or 2. Use the Build Plan information in the request parameters to simply create the instance (either in this zone or - a child zone).""" + a child zone). + """ # TODO(sandy): We'll have to look for richer specs at some point. @@ -79,15 +81,16 @@ class ZoneAwareScheduler(driver.Scheduler): % locals()) else: # TODO(sandy) Provision in child zone ... - LOG.warning(_("Provision to Child Zone not supported (yet)") - % locals()) + LOG.warning(_("Provision to Child Zone not supported (yet)")) pass def select(self, context, request_spec, *args, **kwargs): - """Select returns a list of weights and zone/host information + """ + Select returns a list of weights and zone/host information corresponding to the best hosts to service the request. Any child zone information has been encrypted so as not to reveal - anything about the children.""" + anything about the children. + """ return self._schedule(context, "compute", request_spec, *args, **kwargs) @@ -95,13 +98,15 @@ class ZoneAwareScheduler(driver.Scheduler): # so we don't implement the default "schedule()" method required # of Schedulers. def schedule(self, context, topic, request_spec, *args, **kwargs): - """The schedule() contract requires we return the one + """ + The schedule() contract requires we return the one best-suited host for this request. """ raise driver.NoValidHost(_('No hosts were available')) def _schedule(self, context, topic, request_spec, *args, **kwargs): - """Returns a list of hosts that meet the required specs, + """ + Returns a list of hosts that meet the required specs, ordered by their fitness. """ @@ -137,11 +142,15 @@ class ZoneAwareScheduler(driver.Scheduler): return weighted def filter_hosts(self, num, request_spec): - """Derived classes must override this method and return - a list of hosts in [(hostname, capability_dict)] format.""" + """ + Derived classes must override this method and return + a list of hosts in [(hostname, capability_dict)] format. + """ raise NotImplemented() def weigh_hosts(self, num, request_spec, hosts): - """Derived classes must override this method and return - a lists of hosts in [{weight, hostname}] format.""" + """ + Derived classes must override this method and return + a lists of hosts in [{weight, hostname}] format. + """ raise NotImplemented() -- cgit From 3f911877a2a9facdf153f173b3fb76a18e44a2ac Mon Sep 17 00:00:00 2001 From: Sandy Walsh Date: Fri, 27 May 2011 07:24:02 -0700 Subject: docstrings again and import ordering --- nova/compute/api.py | 30 ++++++++++-------------------- nova/scheduler/host_filter.py | 18 ++++++------------ nova/scheduler/zone_aware_scheduler.py | 20 +++++++------------- 3 files changed, 23 insertions(+), 45 deletions(-) (limited to 'nova') diff --git a/nova/compute/api.py b/nova/compute/api.py index 3e5105050..35d60446c 100644 --- a/nova/compute/api.py +++ b/nova/compute/api.py @@ -88,8 +88,7 @@ class API(base.Base): {"method": "get_network_topic", "args": {'fake': 1}}) def _check_injected_file_quota(self, context, injected_files): - """ - Enforce quota limits on injected files. + """Enforce quota limits on injected files. Raises a QuotaError if any limit is exceeded. """ @@ -137,8 +136,7 @@ class API(base.Base): availability_zone=None, user_data=None, metadata={}, injected_files=None, admin_password=None): - """ - Create the number and type of instances requested. + """Create the number and type of instances requested. Verifies that quota and other arguments are valid. """ @@ -293,8 +291,7 @@ class API(base.Base): return False def ensure_default_security_group(self, context): - """ - Ensure that a context has a security group. + """Ensure that a context has a security group. Creates a security group for the security context if it does not already exist. @@ -328,8 +325,7 @@ class API(base.Base): "args": {"security_group_id": security_group.id}}) def trigger_security_group_members_refresh(self, context, group_id): - """ - Called when a security group gains a new or loses a member. + """Called when a security group gains a new or loses a member. Sends an update request to each compute node for whom this is relevant. @@ -370,8 +366,7 @@ class API(base.Base): "args": {"security_group_id": group_id}}) def update(self, context, instance_id, **kwargs): - """ - Updates the instance in the datastore. + """Updates the instance in the datastore. :param context: The security context :param instance_id: ID of the instance to update @@ -424,8 +419,7 @@ class API(base.Base): @scheduler_api.reroute_compute("get") def routing_get(self, context, instance_id): - """ - A version of get with special routing characteristics. + """A version of get with special routing characteristics. Use this method instead of get() if this is the only operation you intend to to. It will route to novaclient.get if the instance is not @@ -435,8 +429,7 @@ class API(base.Base): def get_all(self, context, project_id=None, reservation_id=None, fixed_ip=None): - """ - Get all instances filtered by one of the given parameters. + """Get all instances filtered by one of the given parameters. If there is no filter and the context is an admin, it will retreive all instances in the system. @@ -463,8 +456,7 @@ class API(base.Base): def _cast_compute_message(self, method, context, instance_id, host=None, params=None): - """ - Generic handler for RPC casts to compute. + """Generic handler for RPC casts to compute. :param params: Optional dictionary of arguments to be passed to the compute worker @@ -483,8 +475,7 @@ class API(base.Base): def _call_compute_message(self, method, context, instance_id, host=None, params=None): - """ - Generic handler for RPC calls to compute. + """Generic handler for RPC calls to compute. :param params: Optional dictionary of arguments to be passed to the compute worker @@ -517,8 +508,7 @@ class API(base.Base): % instance_id) def snapshot(self, context, instance_id, name): - """ - Snapshot the given instance. + """Snapshot the given instance. :returns: A dict containing image metadata """ diff --git a/nova/scheduler/host_filter.py b/nova/scheduler/host_filter.py index 89faace45..4260cbf42 100644 --- a/nova/scheduler/host_filter.py +++ b/nova/scheduler/host_filter.py @@ -69,8 +69,7 @@ class HostFilter(object): class AllHostsFilter(HostFilter): - """ - NOP host filter. Returns all hosts in ZoneManager. + """ NOP host filter. Returns all hosts in ZoneManager. This essentially does what the old Scheduler+Chance used to give us. """ @@ -135,8 +134,7 @@ class InstanceTypeFilter(HostFilter): class JsonFilter(HostFilter): - """ - Host Filter to allow simple JSON-based grammar for + """Host Filter to allow simple JSON-based grammar for selecting hosts. """ @@ -233,8 +231,7 @@ class JsonFilter(HostFilter): return (self._full_name(), json.dumps(query)) def _parse_string(self, string, host, services): - """ - Strings prefixed with $ are capability lookups in the + """Strings prefixed with $ are capability lookups in the form '$service.capability[.subcap*]' """ if not string: @@ -283,8 +280,7 @@ FILTERS = [AllHostsFilter, InstanceTypeFilter, JsonFilter] def choose_host_filter(filter_name=None): - """ - Since the caller may specify which filter to use we need + """Since the caller may specify which filter to use we need to have an authoritative list of what is permissible. This function checks the filter name against a predefined set of acceptable filters. @@ -300,8 +296,7 @@ def choose_host_filter(filter_name=None): class HostFilterScheduler(zone_aware_scheduler.ZoneAwareScheduler): - """ - The HostFilterScheduler uses the HostFilter to filter + """The HostFilterScheduler uses the HostFilter to filter hosts for weighing. The particular filter used may be passed in as an argument or the default will be used. @@ -322,8 +317,7 @@ class HostFilterScheduler(zone_aware_scheduler.ZoneAwareScheduler): return host_filter.filter_hosts(self.zone_manager, query) def weigh_hosts(self, num, request_spec, hosts): - """ - Derived classes must override this method and return + """Derived classes must override this method and return a lists of hosts in [{weight, hostname}] format. """ return [dict(weight=1, hostname=host) for host, caps in hosts] diff --git a/nova/scheduler/zone_aware_scheduler.py b/nova/scheduler/zone_aware_scheduler.py index 236907626..bc67c7794 100644 --- a/nova/scheduler/zone_aware_scheduler.py +++ b/nova/scheduler/zone_aware_scheduler.py @@ -23,8 +23,8 @@ across zones. There are two expansion points to this class for: import operator from nova import db -from nova import rpc from nova import log as logging +from nova import rpc from nova.scheduler import api from nova.scheduler import driver @@ -40,8 +40,7 @@ class ZoneAwareScheduler(driver.Scheduler): def schedule_run_instance(self, context, instance_id, request_spec, *args, **kwargs): - """ - This method is called from nova.compute.api to provision + """This method is called from nova.compute.api to provision an instance. However we need to look at the parameters being passed in to see if this is a request to: 1. Create a Build Plan and then provision, or @@ -85,8 +84,7 @@ class ZoneAwareScheduler(driver.Scheduler): pass def select(self, context, request_spec, *args, **kwargs): - """ - Select returns a list of weights and zone/host information + """Select returns a list of weights and zone/host information corresponding to the best hosts to service the request. Any child zone information has been encrypted so as not to reveal anything about the children. @@ -98,15 +96,13 @@ class ZoneAwareScheduler(driver.Scheduler): # so we don't implement the default "schedule()" method required # of Schedulers. def schedule(self, context, topic, request_spec, *args, **kwargs): - """ - The schedule() contract requires we return the one + """The schedule() contract requires we return the one best-suited host for this request. """ raise driver.NoValidHost(_('No hosts were available')) def _schedule(self, context, topic, request_spec, *args, **kwargs): - """ - Returns a list of hosts that meet the required specs, + """Returns a list of hosts that meet the required specs, ordered by their fitness. """ @@ -142,15 +138,13 @@ class ZoneAwareScheduler(driver.Scheduler): return weighted def filter_hosts(self, num, request_spec): - """ - Derived classes must override this method and return + """Derived classes must override this method and return a list of hosts in [(hostname, capability_dict)] format. """ raise NotImplemented() def weigh_hosts(self, num, request_spec, hosts): - """ - Derived classes must override this method and return + """Derived classes must override this method and return a lists of hosts in [{weight, hostname}] format. """ raise NotImplemented() -- cgit From a9278909cbb6d5ea9283231dbd6efc67b812abff Mon Sep 17 00:00:00 2001 From: Dan Prince Date: Sat, 28 May 2011 23:10:42 -0400 Subject: Update the rebuild_instance function in the compute manager so that it accepts the arguments that our current compute API sends. --- nova/compute/manager.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) (limited to 'nova') diff --git a/nova/compute/manager.py b/nova/compute/manager.py index d1e01f275..3897b3a9e 100644 --- a/nova/compute/manager.py +++ b/nova/compute/manager.py @@ -331,7 +331,7 @@ class ComputeManager(manager.SchedulerDependentManager): @exception.wrap_exception @checks_instance_lock - def rebuild_instance(self, context, instance_id, image_id): + def rebuild_instance(self, context, instance_id, **kwargs): """Destroy and re-make this instance. A 'rebuild' effectively purges all existing data from the system and @@ -349,7 +349,8 @@ class ComputeManager(manager.SchedulerDependentManager): self._update_state(context, instance_id, power_state.BUILDING) self.driver.destroy(instance_ref) - instance_ref.image_id = image_id + instance_ref.image_id = kwargs.get('image_id') + instance_ref.injected_files = kwargs.get('injected_files', []) self.driver.spawn(instance_ref) self._update_image_id(context, instance_id, image_id) -- cgit From ccf522daaca0d4136c072c1905dd9fbaa1dfb2e9 Mon Sep 17 00:00:00 2001 From: Dan Prince Date: Sat, 28 May 2011 23:12:07 -0400 Subject: Fixes to the SQLAlchmeny API such that metadata is saved on an instance_update. Added integration test to test that instance metadata is updated on a rebuild. --- nova/db/sqlalchemy/api.py | 22 +++++++++++++--------- nova/tests/integrated/api/client.py | 10 ++++++++-- nova/tests/integrated/test_servers.py | 34 ++++++++++++++++++++++++++++++++++ 3 files changed, 55 insertions(+), 11 deletions(-) (limited to 'nova') diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index e4dda5c12..1a7cae6e9 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -771,6 +771,15 @@ def fixed_ip_update(context, address, values): ################### +def _metadata_refs(metadata_dict): + metadata_refs = [] + if metadata_dict: + for k, v in metadata_dict.iteritems(): + metadata_ref = models.InstanceMetadata() + metadata_ref['key'] = k + metadata_ref['value'] = v + metadata_refs.append(metadata_ref) + return metadata_refs @require_context @@ -780,15 +789,7 @@ def instance_create(context, values): context - request context object values - dict containing column values. """ - metadata = values.get('metadata') - metadata_refs = [] - if metadata: - for k, v in metadata.iteritems(): - metadata_ref = models.InstanceMetadata() - metadata_ref['key'] = k - metadata_ref['value'] = v - metadata_refs.append(metadata_ref) - values['metadata'] = metadata_refs + values['metadata'] = _metadata_refs(values.get('metadata')) instance_ref = models.Instance() instance_ref.update(values) @@ -1010,6 +1011,9 @@ def instance_set_state(context, instance_id, state, description=None): @require_context def instance_update(context, instance_id, values): session = get_session() + metadata = values.get('metadata') + if metadata: + values['metadata'] = _metadata_refs(values.get('metadata')) with session.begin(): instance_ref = instance_get(context, instance_id, session=session) instance_ref.update(values) diff --git a/nova/tests/integrated/api/client.py b/nova/tests/integrated/api/client.py index 7e20c9b00..eb9a3056e 100644 --- a/nova/tests/integrated/api/client.py +++ b/nova/tests/integrated/api/client.py @@ -152,7 +152,10 @@ class TestOpenStackClient(object): def _decode_json(self, response): body = response.read() LOG.debug(_("Decoding JSON: %s") % (body)) - return json.loads(body) + if body: + return json.loads(body) + else: + return "" def api_get(self, relative_uri, **kwargs): kwargs.setdefault('check_response_status', [200]) @@ -166,7 +169,7 @@ class TestOpenStackClient(object): headers['Content-Type'] = 'application/json' kwargs['body'] = json.dumps(body) - kwargs.setdefault('check_response_status', [200]) + kwargs.setdefault('check_response_status', [200, 202]) response = self.api_request(relative_uri, **kwargs) return self._decode_json(response) @@ -185,6 +188,9 @@ class TestOpenStackClient(object): def post_server(self, server): return self.api_post('/servers', server)['server'] + def post_server_action(self, server_id, data): + return self.api_post('/servers/%s/action' % server_id, data) + def delete_server(self, server_id): return self.api_delete('/servers/%s' % server_id) diff --git a/nova/tests/integrated/test_servers.py b/nova/tests/integrated/test_servers.py index e89d0100a..604faf59f 100644 --- a/nova/tests/integrated/test_servers.py +++ b/nova/tests/integrated/test_servers.py @@ -179,6 +179,40 @@ class ServersTest(integrated_helpers._IntegratedTestBase): # Cleanup self._delete_server(created_server_id) + def test_create_and_rebuild_server_with_metadata(self): + """Rebuild a server with metadata.""" + + # create a server with initially has no metadata + server = self._build_minimal_create_server_request() + server_post = {'server': server} + created_server = self.api.post_server(server_post) + LOG.debug("created_server: %s" % created_server) + self.assertTrue(created_server['id']) + created_server_id = created_server['id'] + + # rebuild the server with metadata + post = {} + post['rebuild'] = { + "imageRef": "https://localhost/v1.1/32278/images/2", + "name": "blah" + } + + metadata = {} + for i in range(30): + metadata['key_%s' % i] = 'value_%s' % i + + post['rebuild']['metadata'] = metadata + + self.api.post_server_action(created_server_id, post) + LOG.debug("rebuilt server: %s" % created_server) + self.assertTrue(created_server['id']) + + found_server = self.api.get_server(created_server_id) + self.assertEqual(created_server_id, found_server['id']) + self.assertEqual(metadata, found_server.get('metadata')) + + # Cleanup + self._delete_server(created_server_id) if __name__ == "__main__": unittest.main() -- cgit From 833481d796db557dddde6b4b9e75b7cf518b88fa Mon Sep 17 00:00:00 2001 From: Dan Prince Date: Sun, 29 May 2011 07:51:44 -0400 Subject: Use metadata variable when calling _metadata_refs. --- nova/db/sqlalchemy/api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'nova') diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index 1a7cae6e9..a678ebedd 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -1013,7 +1013,7 @@ def instance_update(context, instance_id, values): session = get_session() metadata = values.get('metadata') if metadata: - values['metadata'] = _metadata_refs(values.get('metadata')) + values['metadata'] = _metadata_refs(metadata) with session.begin(): instance_ref = instance_get(context, instance_id, session=session) instance_ref.update(values) -- cgit From 45818393a20a56d5e0aab23f3c78e430e0c1167a Mon Sep 17 00:00:00 2001 From: Kei Masumoto Date: Mon, 30 May 2011 14:17:00 +0900 Subject: fixed nova.virt.libvirt_conn.resume() method - removing try-catch --- nova/virt/libvirt_conn.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) (limited to 'nova') diff --git a/nova/virt/libvirt_conn.py b/nova/virt/libvirt_conn.py index 47a77b3ae..32f374955 100644 --- a/nova/virt/libvirt_conn.py +++ b/nova/virt/libvirt_conn.py @@ -569,12 +569,8 @@ class LibvirtConnection(driver.ComputeDriver): @exception.wrap_exception def resume(self, instance, callback): """resume the specified instance""" - try: - dom = self._lookup_by_name(instance.name) - dom.create() - except libvirt.LibvirtError: - xml = self.to_xml(instance, None) - self._create_new_domain(xml) + dom = self._lookup_by_name(instance.name) + dom.create() @exception.wrap_exception def rescue(self, instance): -- cgit From 2155f2b1ab22c6183ab5266e16a675f1469fca50 Mon Sep 17 00:00:00 2001 From: Dan Prince Date: Mon, 30 May 2011 11:29:55 -0400 Subject: Updates so that 'name' can be updated when doing a OS API v1.1 rebuild. Fixed issue where metadata wasn't getting deleted when an empty dict was POST'd on a rebuild. --- nova/api/openstack/servers.py | 10 +++-- nova/compute/api.py | 13 ++++--- nova/db/sqlalchemy/api.py | 17 +++++++-- nova/tests/integrated/test_servers.py | 72 +++++++++++++++++++++++++++++++++++ 4 files changed, 100 insertions(+), 12 deletions(-) (limited to 'nova') diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index 5c10fc916..8e191c232 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -708,14 +708,16 @@ class ControllerV11(Controller): image_id = common.get_id_from_href(image_ref) personalities = info["rebuild"].get("personality", []) - metadata = info["rebuild"].get("metadata", {}) + metadata = info["rebuild"].get("metadata") + name = info["rebuild"].get("name") - self._validate_metadata(metadata) + if metadata: + self._validate_metadata(metadata) self._decode_personalities(personalities) try: - self.compute_api.rebuild(context, instance_id, image_id, metadata, - personalities) + self.compute_api.rebuild(context, instance_id, image_id, name, + metadata, personalities) except exception.BuildInProgress: msg = _("Instance %d is currently being rebuilt.") % instance_id LOG.debug(msg) diff --git a/nova/compute/api.py b/nova/compute/api.py index 4f2363387..151679521 100644 --- a/nova/compute/api.py +++ b/nova/compute/api.py @@ -530,7 +530,7 @@ class API(base.Base): """Reboot the given instance.""" self._cast_compute_message('reboot_instance', context, instance_id) - def rebuild(self, context, instance_id, image_id, metadata=None, + def rebuild(self, context, instance_id, image_id, name=None, metadata=None, files_to_inject=None): """Rebuild the given instance with the provided metadata.""" instance = db.api.instance_get(context, instance_id) @@ -539,13 +539,16 @@ class API(base.Base): msg = _("Instance already building") raise exception.BuildInProgress(msg) - metadata = metadata or {} - self._check_metadata_properties_quota(context, metadata) - files_to_inject = files_to_inject or [] self._check_injected_file_quota(context, files_to_inject) - self.db.instance_update(context, instance_id, {"metadata": metadata}) + values = {} + if metadata is not None: + self._check_metadata_properties_quota(context, metadata) + values['metadata'] = metadata + if name is not None: + values['display_name'] = name + self.db.instance_update(context, instance_id, values) rebuild_params = { "image_id": image_id, diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index a678ebedd..ea84e96e7 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -1012,8 +1012,9 @@ def instance_set_state(context, instance_id, state, description=None): def instance_update(context, instance_id, values): session = get_session() metadata = values.get('metadata') - if metadata: - values['metadata'] = _metadata_refs(metadata) + if metadata is not None: + instance_metadata_update_or_create(context, instance_id, + values.pop('metadata'), True) with session.begin(): instance_ref = instance_get(context, instance_id, session=session) instance_ref.update(values) @@ -2570,8 +2571,12 @@ def instance_metadata_get_item(context, instance_id, key): @require_context -def instance_metadata_update_or_create(context, instance_id, metadata): +def instance_metadata_update_or_create(context, instance_id, metadata, + purge=False): session = get_session() + + original_metadata = instance_metadata_get(context, instance_id) + meta_ref = None for key, value in metadata.iteritems(): try: @@ -2583,4 +2588,10 @@ def instance_metadata_update_or_create(context, instance_id, metadata): "instance_id": instance_id, "deleted": 0}) meta_ref.save(session=session) + + if purge: + for key in original_metadata.keys(): + if not key in metadata.keys(): + instance_metadata_delete(context, instance_id, key) + return metadata diff --git a/nova/tests/integrated/test_servers.py b/nova/tests/integrated/test_servers.py index 604faf59f..a67fa1bb5 100644 --- a/nova/tests/integrated/test_servers.py +++ b/nova/tests/integrated/test_servers.py @@ -179,6 +179,36 @@ class ServersTest(integrated_helpers._IntegratedTestBase): # Cleanup self._delete_server(created_server_id) + def test_create_and_rebuild_server(self): + """Rebuild a server.""" + + # create a server with initially has no metadata + server = self._build_minimal_create_server_request() + server_post = {'server': server} + created_server = self.api.post_server(server_post) + LOG.debug("created_server: %s" % created_server) + self.assertTrue(created_server['id']) + created_server_id = created_server['id'] + + # rebuild the server with metadata + post = {} + post['rebuild'] = { + "imageRef": "https://localhost/v1.1/32278/images/2", + "name": "blah" + } + + self.api.post_server_action(created_server_id, post) + LOG.debug("rebuilt server: %s" % created_server) + self.assertTrue(created_server['id']) + + found_server = self.api.get_server(created_server_id) + self.assertEqual(created_server_id, found_server['id']) + self.assertEqual({}, found_server.get('metadata')) + self.assertEqual('blah', found_server.get('name')) + + # Cleanup + self._delete_server(created_server_id) + def test_create_and_rebuild_server_with_metadata(self): """Rebuild a server with metadata.""" @@ -210,9 +240,51 @@ class ServersTest(integrated_helpers._IntegratedTestBase): found_server = self.api.get_server(created_server_id) self.assertEqual(created_server_id, found_server['id']) self.assertEqual(metadata, found_server.get('metadata')) + self.assertEqual('blah', found_server.get('name')) + + # Cleanup + self._delete_server(created_server_id) + + def test_create_and_rebuild_server_with_metadata_removal(self): + """Rebuild a server with metadata.""" + + # create a server with initially has no metadata + server = self._build_minimal_create_server_request() + server_post = {'server': server} + + metadata = {} + for i in range(30): + metadata['key_%s' % i] = 'value_%s' % i + + server_post['server']['metadata'] = metadata + + created_server = self.api.post_server(server_post) + LOG.debug("created_server: %s" % created_server) + self.assertTrue(created_server['id']) + created_server_id = created_server['id'] + + # rebuild the server with metadata + post = {} + post['rebuild'] = { + "imageRef": "https://localhost/v1.1/32278/images/2", + "name": "blah" + } + + metadata = {} + post['rebuild']['metadata'] = metadata + + self.api.post_server_action(created_server_id, post) + LOG.debug("rebuilt server: %s" % created_server) + self.assertTrue(created_server['id']) + + found_server = self.api.get_server(created_server_id) + self.assertEqual(created_server_id, found_server['id']) + self.assertEqual(metadata, found_server.get('metadata')) + self.assertEqual('blah', found_server.get('name')) # Cleanup self._delete_server(created_server_id) + if __name__ == "__main__": unittest.main() -- cgit From 7beafb1aafac97e6dfc28108062785465cc8f577 Mon Sep 17 00:00:00 2001 From: Dan Prince Date: Tue, 31 May 2011 14:38:12 -0400 Subject: Use a new instance_metadata_delete_all DB api call to delete existing metadata when updating a server. --- nova/db/sqlalchemy/api.py | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) (limited to 'nova') diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index ea84e96e7..8df96cbf4 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -1013,8 +1013,9 @@ def instance_update(context, instance_id, values): session = get_session() metadata = values.get('metadata') if metadata is not None: + instance_metadata_delete_all(context, instance_id) instance_metadata_update_or_create(context, instance_id, - values.pop('metadata'), True) + values.pop('metadata')) with session.begin(): instance_ref = instance_get(context, instance_id, session=session) instance_ref.update(values) @@ -2554,6 +2555,17 @@ def instance_metadata_delete(context, instance_id, key): 'updated_at': literal_column('updated_at')}) +@require_context +def instance_metadata_delete_all(context, instance_id): + session = get_session() + session.query(models.InstanceMetadata).\ + filter_by(instance_id=instance_id).\ + filter_by(deleted=False).\ + update({'deleted': True, + 'deleted_at': datetime.datetime.utcnow(), + 'updated_at': literal_column('updated_at')}) + + @require_context def instance_metadata_get_item(context, instance_id, key): session = get_session() @@ -2571,8 +2583,7 @@ def instance_metadata_get_item(context, instance_id, key): @require_context -def instance_metadata_update_or_create(context, instance_id, metadata, - purge=False): +def instance_metadata_update_or_create(context, instance_id, metadata): session = get_session() original_metadata = instance_metadata_get(context, instance_id) @@ -2589,9 +2600,4 @@ def instance_metadata_update_or_create(context, instance_id, metadata, "deleted": 0}) meta_ref.save(session=session) - if purge: - for key in original_metadata.keys(): - if not key in metadata.keys(): - instance_metadata_delete(context, instance_id, key) - return metadata -- cgit