summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJustin Santa Barbara <justin@fathomdb.com>2011-03-23 19:00:25 -0700
committerJustin Santa Barbara <justin@fathomdb.com>2011-03-23 19:00:25 -0700
commit21f3fc7cecb4c2f0657bb6bc78098910dca2e6b7 (patch)
tree96bada7d30eb2325f56328f54dcc0da9ad07ac12
parent3c295817f91eb7c76a64d157ff4a938c85075a36 (diff)
parent0759158d08f3f98456ae094fd9d94d36b98216a5 (diff)
Merged with lp:nova, fixed conflicts
-rw-r--r--nova/api/ec2/admin.py3
-rw-r--r--nova/api/openstack/images.py6
-rw-r--r--nova/api/openstack/servers.py1
-rw-r--r--nova/api/openstack/views/servers.py8
-rw-r--r--nova/compute/api.py42
-rw-r--r--nova/compute/manager.py70
-rw-r--r--nova/db/sqlalchemy/api.py8
-rw-r--r--nova/db/sqlalchemy/migrate_repo/versions/013_add_flavors_to_migrations.py50
-rw-r--r--nova/db/sqlalchemy/models.py2
-rw-r--r--nova/image/glance.py36
-rw-r--r--nova/image/service.py16
-rw-r--r--nova/network/linux_net.py44
-rw-r--r--nova/tests/api/openstack/fakes.py20
-rw-r--r--nova/tests/api/openstack/test_images.py44
-rw-r--r--nova/tests/api/openstack/test_servers.py22
-rw-r--r--nova/tests/image/__init__.py16
-rw-r--r--nova/tests/image/test_glance.py188
-rw-r--r--nova/tests/test_compute.py59
-rw-r--r--nova/tests/test_localization.py3
-rw-r--r--nova/tests/test_misc.py43
-rw-r--r--nova/tests/test_virt.py2
-rw-r--r--nova/tests/xenapi/stubs.py7
-rw-r--r--nova/utils.py74
-rw-r--r--nova/virt/libvirt_conn.py25
-rw-r--r--nova/virt/xenapi/vmops.py55
-rw-r--r--nova/virt/xenapi_conn.py12
-rw-r--r--plugins/xenserver/xenapi/etc/xapi.d/plugins/migration3
27 files changed, 697 insertions, 162 deletions
diff --git a/nova/api/ec2/admin.py b/nova/api/ec2/admin.py
index 037184b40..d8d90ad83 100644
--- a/nova/api/ec2/admin.py
+++ b/nova/api/ec2/admin.py
@@ -120,7 +120,8 @@ class AdminController(object):
def describe_instance_types(self, context, **_kwargs):
"""Returns all active instance types data (vcpus, memory, etc.)"""
- return {'instanceTypeSet': [db.instance_type_get_all(context)]}
+ return {'instanceTypeSet': [instance_dict(v) for v in
+ db.instance_type_get_all(context).values()]}
def describe_user(self, _context, name, **_kwargs):
"""Returns user data, including access and secret keys."""
diff --git a/nova/api/openstack/images.py b/nova/api/openstack/images.py
index 98f0dd96b..99c14275a 100644
--- a/nova/api/openstack/images.py
+++ b/nova/api/openstack/images.py
@@ -143,6 +143,7 @@ class Controller(wsgi.Controller):
image = self._service.show(req.environ['nova.context'], image_id)
_convert_image_id_to_hash(image)
+ self._format_image_dates(image)
return dict(image=image)
def delete(self, req, id):
@@ -164,3 +165,8 @@ class Controller(wsgi.Controller):
# Users may not modify public images, and that's all that
# we support for now.
raise faults.Fault(exc.HTTPNotFound())
+
+ def _format_image_dates(self, image):
+ for attr in ['created_at', 'updated_at', 'deleted_at']:
+ if image.get(attr) is not None:
+ image[attr] = image[attr].strftime('%Y-%m-%dT%H:%M:%SZ')
diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py
index 830bc2659..d392ab57f 100644
--- a/nova/api/openstack/servers.py
+++ b/nova/api/openstack/servers.py
@@ -22,6 +22,7 @@ from xml.dom import minidom
from webob import exc
from nova import compute
+from nova import context
from nova import exception
from nova import flags
from nova import log as logging
diff --git a/nova/api/openstack/views/servers.py b/nova/api/openstack/views/servers.py
index 261acfed0..68f712e56 100644
--- a/nova/api/openstack/views/servers.py
+++ b/nova/api/openstack/views/servers.py
@@ -16,7 +16,10 @@
# under the License.
import hashlib
+
from nova.compute import power_state
+import nova.compute
+import nova.context
from nova.api.openstack import common
from nova.api.openstack.views import addresses as addresses_view
from nova.api.openstack.views import flavors as flavors_view
@@ -84,7 +87,12 @@ class ViewBuilder(object):
for k, v in mapped_keys.iteritems():
inst_dict[k] = inst[v]
+ ctxt = nova.context.get_admin_context()
inst_dict['status'] = power_mapping[inst_dict['status']]
+ compute_api = nova.compute.API()
+ if compute_api.has_finished_migration(ctxt, inst['id']):
+ inst_dict['status'] = 'resize-confirm'
+
inst_dict['addresses'] = self.addresses_builder.build(inst)
# Return the metadata as a dictionary
diff --git a/nova/compute/api.py b/nova/compute/api.py
index 32577af82..01eead4ac 100644
--- a/nova/compute/api.py
+++ b/nova/compute/api.py
@@ -253,6 +253,16 @@ class API(base.Base):
return [dict(x.iteritems()) for x in instances]
+ def has_finished_migration(self, context, instance_id):
+ """Retrieves whether or not a finished migration exists for
+ an instance"""
+ try:
+ db.migration_get_by_instance_and_status(context, instance_id,
+ 'finished')
+ return True
+ except exception.NotFound:
+ return False
+
def ensure_default_security_group(self, context):
""" Create security group for the security context if it
does not already exist
@@ -464,6 +474,8 @@ class API(base.Base):
params = {'migration_id': migration_ref['id']}
self._cast_compute_message('revert_resize', context, instance_id,
migration_ref['dest_compute'], params=params)
+ self.db.migration_update(context, migration_ref['id'],
+ {'status': 'reverted'})
def confirm_resize(self, context, instance_id):
"""Confirms a migration/resize, deleting the 'old' instance in the
@@ -479,17 +491,41 @@ class API(base.Base):
self._cast_compute_message('confirm_resize', context, instance_id,
migration_ref['source_compute'], params=params)
- self.db.migration_update(context, migration_id,
+ self.db.migration_update(context, migration_ref['id'],
{'status': 'confirmed'})
self.db.instance_update(context, instance_id,
{'host': migration_ref['dest_compute'], })
- def resize(self, context, instance_id, flavor):
+ def resize(self, context, instance_id, flavor_id):
"""Resize a running instance."""
+ instance = self.db.instance_get(context, instance_id)
+ current_instance_type = self.db.instance_type_get_by_name(
+ context, instance['instance_type'])
+
+ new_instance_type = self.db.instance_type_get_by_flavor_id(
+ context, flavor_id)
+ current_instance_type_name = current_instance_type['name']
+ new_instance_type_name = new_instance_type['name']
+ LOG.debug(_("Old instance type %(current_instance_type_name)s, "
+ " new instance type %(new_instance_type_name)s") % locals())
+ if not new_instance_type:
+ raise exception.ApiError(_("Requested flavor %(flavor_id)d "
+ "does not exist") % locals())
+
+ current_memory_mb = current_instance_type['memory_mb']
+ new_memory_mb = new_instance_type['memory_mb']
+ if current_memory_mb > new_memory_mb:
+ raise exception.ApiError(_("Invalid flavor: cannot downsize"
+ "instances"))
+ if current_memory_mb == new_memory_mb:
+ raise exception.ApiError(_("Invalid flavor: cannot use"
+ "the same flavor. "))
+
self._cast_scheduler_message(context,
{"method": "prep_resize",
"args": {"topic": FLAGS.compute_topic,
- "instance_id": instance_id, }},)
+ "instance_id": instance_id,
+ "flavor_id": flavor_id}})
def pause(self, context, instance_id):
"""Pause the given instance."""
diff --git a/nova/compute/manager.py b/nova/compute/manager.py
index b67b27dd0..052db5ac6 100644
--- a/nova/compute/manager.py
+++ b/nova/compute/manager.py
@@ -441,26 +441,41 @@ class ComputeManager(manager.Manager):
instance_ref = self.db.instance_get(context, instance_id)
migration_ref = self.db.migration_get(context, migration_id)
- #TODO(mdietz): we may want to split these into separate methods.
- if migration_ref['source_compute'] == FLAGS.host:
- #NOTE(justinsb): Naughty calling of internal method
- self.driver._start(instance_ref)
- self.db.migration_update(context, migration_id,
- {'status': 'reverted'})
- else:
- self.driver.destroy(instance_ref)
- topic = self.db.queue_get_for(context, FLAGS.compute_topic,
- instance_ref['host'])
- rpc.cast(context, topic,
- {'method': 'revert_resize',
- 'args': {
- 'migration_id': migration_ref['id'],
- 'instance_id': instance_id, },
- })
+ self.driver.destroy(instance_ref)
+ topic = self.db.queue_get_for(context, FLAGS.compute_topic,
+ instance_ref['host'])
+ rpc.cast(context, topic,
+ {'method': 'finish_revert_resize',
+ 'args': {
+ 'migration_id': migration_ref['id'],
+ 'instance_id': instance_id, },
+ })
+
+ @exception.wrap_exception
+ @checks_instance_lock
+ def finish_revert_resize(self, context, instance_id, migration_id):
+ """Finishes the second half of reverting a resize, powering back on
+ the source instance and reverting the resized attributes in the
+ database"""
+ instance_ref = self.db.instance_get(context, instance_id)
+ migration_ref = self.db.migration_get(context, migration_id)
+ instance_type = self.db.instance_type_get_by_flavor_id(context,
+ migration_ref['old_flavor_id'])
+
+ # Just roll back the record. There's no need to resize down since
+ # the 'old' VM already has the preferred attributes
+ self.db.instance_update(context, instance_id,
+ dict(memory_mb=instance_type['memory_mb'],
+ vcpus=instance_type['vcpus'],
+ local_gb=instance_type['local_gb']))
+
+ self.driver.revert_resize(instance_ref)
+ self.db.migration_update(context, migration_id,
+ {'status': 'reverted'})
@exception.wrap_exception
@checks_instance_lock
- def prep_resize(self, context, instance_id):
+ def prep_resize(self, context, instance_id, flavor_id):
"""Initiates the process of moving a running instance to another
host, possibly changing the RAM and disk size in the process"""
context = context.elevated()
@@ -469,12 +484,17 @@ class ComputeManager(manager.Manager):
raise exception.Error(_(
'Migration error: destination same as source!'))
+ instance_type = self.db.instance_type_get_by_flavor_id(context,
+ flavor_id)
migration_ref = self.db.migration_create(context,
{'instance_id': instance_id,
'source_compute': instance_ref['host'],
'dest_compute': FLAGS.host,
'dest_host': self.driver.get_host_ip_addr(),
+ 'old_flavor_id': instance_type['flavorid'],
+ 'new_flavor_id': flavor_id,
'status': 'pre-migrating'})
+
LOG.audit(_('instance %s: migrating to '), instance_id,
context=context)
topic = self.db.queue_get_for(context, FLAGS.compute_topic,
@@ -500,8 +520,6 @@ class ComputeManager(manager.Manager):
self.db.migration_update(context, migration_id,
{'status': 'post-migrating', })
- #TODO(mdietz): This is where we would update the VM record
- #after resizing
service = self.db.service_get_by_host_and_topic(context,
migration_ref['dest_compute'], FLAGS.compute_topic)
topic = self.db.queue_get_for(context, FLAGS.compute_topic,
@@ -522,7 +540,19 @@ class ComputeManager(manager.Manager):
migration_ref = self.db.migration_get(context, migration_id)
instance_ref = self.db.instance_get(context,
migration_ref['instance_id'])
-
+ # TODO(mdietz): apply the rest of the instance_type attributes going
+ # after they're supported
+ instance_type = self.db.instance_type_get_by_flavor_id(context,
+ migration_ref['new_flavor_id'])
+ self.db.instance_update(context, instance_id,
+ dict(instance_type=instance_type['name'],
+ memory_mb=instance_type['memory_mb'],
+ vcpus=instance_type['vcpus'],
+ local_gb=instance_type['local_gb']))
+
+ # reload the updated instance ref
+ # FIXME(mdietz): is there reload functionality?
+ instance_ref = self.db.instance_get(context, instance_id)
self.driver.finish_resize(instance_ref, disk_info)
self.db.migration_update(context, migration_id,
diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py
index 3bf4f5eb8..d7b5aff46 100644
--- a/nova/db/sqlalchemy/api.py
+++ b/nova/db/sqlalchemy/api.py
@@ -2220,8 +2220,8 @@ def migration_get_by_instance_and_status(context, instance_id, status):
filter_by(instance_id=instance_id).\
filter_by(status=status).first()
if not result:
- raise exception.NotFound(_("No migration found with instance id %s")
- % migration_id)
+ raise exception.NotFound(_("No migration found for instance "
+ "%(instance_id)s with status %(status)s") % locals())
return result
@@ -2336,8 +2336,8 @@ def instance_type_create(_context, values):
instance_type_ref = models.InstanceTypes()
instance_type_ref.update(values)
instance_type_ref.save()
- except:
- raise exception.DBError
+ except Exception, e:
+ raise exception.DBError(e)
return instance_type_ref
diff --git a/nova/db/sqlalchemy/migrate_repo/versions/013_add_flavors_to_migrations.py b/nova/db/sqlalchemy/migrate_repo/versions/013_add_flavors_to_migrations.py
new file mode 100644
index 000000000..3fb92e85c
--- /dev/null
+++ b/nova/db/sqlalchemy/migrate_repo/versions/013_add_flavors_to_migrations.py
@@ -0,0 +1,50 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2011 OpenStack LLC.
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.from sqlalchemy import *
+
+from sqlalchemy import *
+from migrate import *
+
+from nova import log as logging
+
+
+meta = MetaData()
+
+migrations = Table('migrations', meta,
+ Column('id', Integer(), primary_key=True, nullable=False),
+ )
+
+#
+# Tables to alter
+#
+#
+
+old_flavor_id = Column('old_flavor_id', Integer())
+new_flavor_id = Column('new_flavor_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
+ migrations.create_column(old_flavor_id)
+ migrations.create_column(new_flavor_id)
+
+
+def downgrade(migrate_engine):
+ meta.bind = migrate_engine
+ migrations.drop_column(old_flavor_id)
+ migrations.drop_column(new_flavor_id)
diff --git a/nova/db/sqlalchemy/models.py b/nova/db/sqlalchemy/models.py
index fa695f25d..3b95ac23e 100644
--- a/nova/db/sqlalchemy/models.py
+++ b/nova/db/sqlalchemy/models.py
@@ -436,6 +436,8 @@ class Migration(BASE, NovaBase):
source_compute = Column(String(255))
dest_compute = Column(String(255))
dest_host = Column(String(255))
+ old_flavor_id = Column(Integer())
+ new_flavor_id = Column(Integer())
instance_id = Column(Integer, ForeignKey('instances.id'), nullable=True)
#TODO(_cerberus_): enum
status = Column(String(255))
diff --git a/nova/image/glance.py b/nova/image/glance.py
index 15fca69b8..171b28fde 100644
--- a/nova/image/glance.py
+++ b/nova/image/glance.py
@@ -18,6 +18,8 @@
from __future__ import absolute_import
+import datetime
+
from glance.common import exception as glance_exception
from nova import exception
@@ -37,8 +39,11 @@ GlanceClient = utils.import_class('glance.client.Client')
class GlanceImageService(service.BaseImageService):
"""Provides storage and retrieval of disk image objects within Glance."""
- def __init__(self):
- self.client = GlanceClient(FLAGS.glance_host, FLAGS.glance_port)
+ def __init__(self, client=None):
+ if client is None:
+ self.client = GlanceClient(FLAGS.glance_host, FLAGS.glance_port)
+ else:
+ self.client = client
def index(self, context):
"""
@@ -50,7 +55,8 @@ class GlanceImageService(service.BaseImageService):
"""
Calls out to Glance for a list of detailed image information
"""
- return self.client.get_images_detailed()
+ return [self._convert_timestamps_to_datetimes(image)
+ for image in self.client.get_images_detailed()]
def show(self, context, image_id):
"""
@@ -60,8 +66,23 @@ class GlanceImageService(service.BaseImageService):
image = self.client.get_image_meta(image_id)
except glance_exception.NotFound:
raise exception.NotFound
+ return self._convert_timestamps_to_datetimes(image)
+
+ def _convert_timestamps_to_datetimes(self, image):
+ """
+ Returns image with known timestamp fields converted to datetime objects
+ """
+ for attr in ['created_at', 'updated_at', 'deleted_at']:
+ if image.get(attr) is not None:
+ image[attr] = self._parse_glance_iso8601_timestamp(image[attr])
return image
+ def _parse_glance_iso8601_timestamp(self, timestamp):
+ """
+ Parse a subset of iso8601 timestamps into datetime objects
+ """
+ return datetime.datetime.strptime(timestamp, "%Y-%m-%dT%H:%M:%S.%f")
+
def show_by_name(self, context, name):
"""
Returns a dict containing image data for the given name.
@@ -88,7 +109,7 @@ class GlanceImageService(service.BaseImageService):
raise exception.NotFound
for chunk in image_chunks:
data.write(chunk)
- return metadata
+ return self._convert_timestamps_to_datetimes(metadata)
def create(self, context, metadata, data=None):
"""
@@ -97,7 +118,8 @@ class GlanceImageService(service.BaseImageService):
:raises AlreadyExists if the image already exist.
"""
- return self.client.add_image(metadata, data)
+ return self._convert_timestamps_to_datetimes(
+ self.client.add_image(metadata, data))
def update(self, context, image_id, metadata, data=None):
"""Replace the contents of the given image with the new data.
@@ -106,10 +128,10 @@ class GlanceImageService(service.BaseImageService):
"""
try:
- result = self.client.update_image(image_id, metadata, data)
+ metadata = self.client.update_image(image_id, metadata, data)
except glance_exception.NotFound:
raise exception.NotFound
- return result
+ return self._convert_timestamps_to_datetimes(metadata)
def delete(self, context, image_id):
"""
diff --git a/nova/image/service.py b/nova/image/service.py
index c09052cab..e907381c9 100644
--- a/nova/image/service.py
+++ b/nova/image/service.py
@@ -40,9 +40,9 @@ class BaseImageService(object):
:retval: a sequence of mappings with the following signature
{'id': opaque id of image,
'name': name of image,
- 'created_at': creation timestamp,
- 'updated_at': modification timestamp,
- 'deleted_at': deletion timestamp or None,
+ 'created_at': creation datetime object,
+ 'updated_at': modification datetime object,
+ 'deleted_at': deletion datetime object or None,
'deleted': boolean indicating if image has been deleted,
'status': string description of image status,
'is_public': boolean indicating if image is public
@@ -64,9 +64,9 @@ class BaseImageService(object):
{'id': opaque id of image,
'name': name of image,
- 'created_at': creation timestamp,
- 'updated_at': modification timestamp,
- 'deleted_at': deletion timestamp or None,
+ 'created_at': creation datetime object,
+ 'updated_at': modification datetime object,
+ 'deleted_at': deletion datetime object or None,
'deleted': boolean indicating if image has been deleted,
'status': string description of image status,
'is_public': boolean indicating if image is public
@@ -88,7 +88,7 @@ class BaseImageService(object):
def create(self, context, metadata, data=None):
"""
- Store the image metadata and data and return the new image id.
+ Store the image metadata and data and return the new image metadata.
:raises AlreadyExists if the image already exist.
@@ -96,7 +96,7 @@ class BaseImageService(object):
raise NotImplementedError
def update(self, context, image_id, metadata, data=None):
- """Update the given image with the new metadata and data.
+ """Update the given image metadata and data and return the metadata
:raises NotFound if the image does not exist.
diff --git a/nova/network/linux_net.py b/nova/network/linux_net.py
index 05cd565a2..796d6ba31 100644
--- a/nova/network/linux_net.py
+++ b/nova/network/linux_net.py
@@ -21,8 +21,6 @@ import inspect
import os
import calendar
-from eventlet import semaphore
-
from nova import db
from nova import exception
from nova import flags
@@ -272,37 +270,30 @@ class IptablesManager(object):
self.ipv4['nat'].add_chain('floating-snat')
self.ipv4['nat'].add_rule('snat', '-j $floating-snat')
- self.semaphore = semaphore.Semaphore()
-
- @utils.synchronized('iptables')
+ @utils.synchronized('iptables', external=True)
def apply(self):
"""Apply the current in-memory set of iptables rules
This will blow away any rules left over from previous runs of the
same component of Nova, and replace them with our current set of
rules. This happens atomically, thanks to iptables-restore.
-
- We wrap the call in a semaphore lock, so that we don't race with
- ourselves. In the event of a race with another component running
- an iptables-* command at the same time, we retry up to 5 times.
"""
- with self.semaphore:
- s = [('iptables', self.ipv4)]
- if FLAGS.use_ipv6:
- s += [('ip6tables', self.ipv6)]
-
- for cmd, tables in s:
- for table in tables:
- current_table, _ = self.execute('sudo',
- '%s-save' % (cmd,),
- '-t', '%s' % (table,),
- attempts=5)
- current_lines = current_table.split('\n')
- new_filter = self._modify_rules(current_lines,
- tables[table])
- self.execute('sudo', '%s-restore' % (cmd,),
- process_input='\n'.join(new_filter),
- attempts=5)
+ s = [('iptables', self.ipv4)]
+ if FLAGS.use_ipv6:
+ s += [('ip6tables', self.ipv6)]
+
+ for cmd, tables in s:
+ for table in tables:
+ current_table, _ = self.execute('sudo',
+ '%s-save' % (cmd,),
+ '-t', '%s' % (table,),
+ attempts=5)
+ current_lines = current_table.split('\n')
+ new_filter = self._modify_rules(current_lines,
+ tables[table])
+ self.execute('sudo', '%s-restore' % (cmd,),
+ process_input='\n'.join(new_filter),
+ attempts=5)
def _modify_rules(self, current_lines, table, binary=None):
unwrapped_chains = table.unwrapped_chains
@@ -595,6 +586,7 @@ def update_dhcp(context, network_id):
_execute(*command, addl_env=env)
+@utils.synchronized('radvd_start')
def update_ra(context, network_id):
network_ref = db.network_get(context, network_id)
diff --git a/nova/tests/api/openstack/fakes.py b/nova/tests/api/openstack/fakes.py
index 75eade4d0..d931b6efb 100644
--- a/nova/tests/api/openstack/fakes.py
+++ b/nova/tests/api/openstack/fakes.py
@@ -15,6 +15,7 @@
# License for the specific language governing permissions and limitations
# under the License.
+import copy
import datetime
import json
import random
@@ -150,22 +151,23 @@ def stub_out_glance(stubs, initial_fixtures=None):
for f in self.fixtures]
def fake_get_images_detailed(self):
- return self.fixtures
+ return copy.deepcopy(self.fixtures)
def fake_get_image_meta(self, image_id):
- for f in self.fixtures:
- if f['id'] == image_id:
- return f
+ image = self._find_image(image_id)
+ if image:
+ return copy.deepcopy(image)
raise glance_exc.NotFound
def fake_add_image(self, image_meta, data=None):
+ image_meta = copy.deepcopy(image_meta)
id = ''.join(random.choice(string.letters) for _ in range(20))
image_meta['id'] = id
self.fixtures.append(image_meta)
return image_meta
def fake_update_image(self, image_id, image_meta, data=None):
- f = self.fake_get_image_meta(image_id)
+ f = self._find_image(image_id)
if not f:
raise glance_exc.NotFound
@@ -173,7 +175,7 @@ def stub_out_glance(stubs, initial_fixtures=None):
return f
def fake_delete_image(self, image_id):
- f = self.fake_get_image_meta(image_id)
+ f = self._find_image(image_id)
if not f:
raise glance_exc.NotFound
@@ -182,6 +184,12 @@ def stub_out_glance(stubs, initial_fixtures=None):
##def fake_delete_all(self):
## self.fixtures = []
+ def _find_image(self, image_id):
+ for f in self.fixtures:
+ if f['id'] == image_id:
+ return f
+ return None
+
GlanceClient = glance_client.Client
fake = FakeGlanceClient(initial_fixtures)
diff --git a/nova/tests/api/openstack/test_images.py b/nova/tests/api/openstack/test_images.py
index a674ccefe..feb32ed9f 100644
--- a/nova/tests/api/openstack/test_images.py
+++ b/nova/tests/api/openstack/test_images.py
@@ -189,13 +189,13 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
"""Test of the OpenStack API /images application controller"""
# Registered images at start of each test.
-
+ now = datetime.datetime.utcnow()
IMAGE_FIXTURES = [
{'id': '23g2ogk23k4hhkk4k42l',
'imageId': '23g2ogk23k4hhkk4k42l',
'name': 'public image #1',
- 'created_at': str(datetime.datetime.utcnow()),
- 'updated_at': str(datetime.datetime.utcnow()),
+ 'created_at': now.isoformat(),
+ 'updated_at': now.isoformat(),
'deleted_at': None,
'deleted': False,
'is_public': True,
@@ -204,8 +204,8 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
{'id': 'slkduhfas73kkaskgdas',
'imageId': 'slkduhfas73kkaskgdas',
'name': 'public image #2',
- 'created_at': str(datetime.datetime.utcnow()),
- 'updated_at': str(datetime.datetime.utcnow()),
+ 'created_at': now.isoformat(),
+ 'updated_at': now.isoformat(),
'deleted_at': None,
'deleted': False,
'is_public': True,
@@ -247,20 +247,20 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
res = req.get_response(fakes.wsgi_app())
res_dict = json.loads(res.body)
- def _is_equivalent_subset(x, y):
- if set(x) <= set(y):
- for k, v in x.iteritems():
- if x[k] != y[k]:
- if x[k] == 'active' and y[k] == 'available':
- continue
- return False
- return True
- return False
-
- for image in res_dict['images']:
- for image_fixture in self.IMAGE_FIXTURES:
- if _is_equivalent_subset(image, image_fixture):
- break
- else:
- self.assertEquals(1, 2, "image %s not in fixtures!" %
- str(image))
+ for image in self.IMAGE_FIXTURES:
+ expected = {
+ 'id': abs(hash(image['imageId'])),
+ 'name': image['name'],
+ 'status': 'active',
+ }
+ self.assertTrue(expected in res_dict['images'])
+
+ def test_show_image(self):
+ expected = self.IMAGE_FIXTURES[0]
+ id = abs(hash(expected['id']))
+ expected_time = self.now.strftime('%Y-%m-%dT%H:%M:%SZ')
+ req = webob.Request.blank('/v1.0/images/%s' % id)
+ res = req.get_response(fakes.wsgi_app())
+ actual = json.loads(res.body)['image']
+ self.assertEqual(expected_time, actual['created_at'])
+ self.assertEqual(expected_time, actual['updated_at'])
diff --git a/nova/tests/api/openstack/test_servers.py b/nova/tests/api/openstack/test_servers.py
index efba2970f..3a804c649 100644
--- a/nova/tests/api/openstack/test_servers.py
+++ b/nova/tests/api/openstack/test_servers.py
@@ -524,16 +524,6 @@ class ServersTest(test.TestCase):
req.body = json.dumps(body)
res = req.get_response(fakes.wsgi_app())
- def test_server_resize(self):
- body = dict(server=dict(
- name='server_test', imageId=2, flavorId=2, metadata={},
- personality={}))
- req = webob.Request.blank('/v1.0/servers/1/action')
- req.method = 'POST'
- req.content_type = 'application/json'
- req.body = json.dumps(body)
- res = req.get_response(fakes.wsgi_app())
-
def test_delete_server_instance(self):
req = webob.Request.blank('/v1.0/servers/1')
req.method = 'DELETE'
@@ -589,6 +579,18 @@ class ServersTest(test.TestCase):
res = req.get_response(fakes.wsgi_app())
self.assertEqual(res.status_int, 400)
+ def test_resized_server_has_correct_status(self):
+ req = self.webreq('/1', 'GET', dict(resize=dict(flavorId=3)))
+
+ def fake_migration_get(*args):
+ return {}
+
+ self.stubs.Set(nova.db, 'migration_get_by_instance_and_status',
+ fake_migration_get)
+ res = req.get_response(fakes.wsgi_app())
+ body = json.loads(res.body)
+ self.assertEqual(body['server']['status'], 'resize-confirm')
+
def test_confirm_resize_server(self):
req = self.webreq('/1/action', 'POST', dict(confirmResize=None))
diff --git a/nova/tests/image/__init__.py b/nova/tests/image/__init__.py
new file mode 100644
index 000000000..b94e2e54e
--- /dev/null
+++ b/nova/tests/image/__init__.py
@@ -0,0 +1,16 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2011 Openstack LLC.
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
diff --git a/nova/tests/image/test_glance.py b/nova/tests/image/test_glance.py
new file mode 100644
index 000000000..f1f8504f3
--- /dev/null
+++ b/nova/tests/image/test_glance.py
@@ -0,0 +1,188 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2011 Openstack LLC.
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+
+import datetime
+import unittest
+
+from nova.image import glance
+
+
+class StubGlanceClient(object):
+
+ def __init__(self, images, add_response=None, update_response=None):
+ self.images = images
+ self.add_response = add_response
+ self.update_response = update_response
+
+ def get_image_meta(self, id):
+ return self.images[id]
+
+ def get_images_detailed(self):
+ return self.images.itervalues()
+
+ def get_image(self, id):
+ return self.images[id], []
+
+ def add_image(self, metadata, data):
+ return self.add_response
+
+ def update_image(self, image_id, metadata, data):
+ return self.update_response
+
+
+class NullWriter(object):
+
+ def write(self, *arg, **kwargs):
+ pass
+
+
+class TestGlanceImageServiceDatetimes(unittest.TestCase):
+
+ def setUp(self):
+ self.client = StubGlanceClient(None)
+ self.service = glance.GlanceImageService(self.client)
+
+ def test_show_passes_through_to_client(self):
+ self.client.images = {'xyz': {'foo': 'bar'}}
+ self.assertEqual(self.service.show({}, 'xyz'), {'foo': 'bar'})
+
+ def test_detail_passes_through_to_client(self):
+ self.client.images = {1: {'foo': 'bar'}}
+ self.assertEqual(list(self.service.detail({})), [{'foo': 'bar'}])
+
+ def test_show_makes_create_datetimes(self):
+ create_time = datetime.datetime.utcnow()
+ self.client.images = {'xyz': {
+ 'id': "id",
+ 'name': "my awesome image",
+ 'created_at': create_time.isoformat(),
+ }}
+ actual = self.service.show({}, 'xyz')
+ self.assertEqual(actual['created_at'], create_time)
+
+ def test_show_makes_update_datetimes(self):
+ update_time = datetime.datetime.utcnow()
+ self.client.images = {'abc': {
+ 'id': "id",
+ 'name': "my okay image",
+ 'updated_at': update_time.isoformat(),
+ }}
+ actual = self.service.show({}, 'abc')
+ self.assertEqual(actual['updated_at'], update_time)
+
+ def test_show_makes_delete_datetimes(self):
+ delete_time = datetime.datetime.utcnow()
+ self.client.images = {'123': {
+ 'id': "123",
+ 'name': "my lame image",
+ 'deleted_at': delete_time.isoformat(),
+ }}
+ actual = self.service.show({}, '123')
+ self.assertEqual(actual['deleted_at'], delete_time)
+
+ def test_show_handles_deleted_at_none(self):
+ self.client.images = {'747': {
+ 'id': "747",
+ 'name': "not deleted",
+ 'deleted_at': None,
+ }}
+ actual = self.service.show({}, '747')
+ self.assertEqual(actual['deleted_at'], None)
+
+ def test_detail_handles_timestamps(self):
+ now = datetime.datetime.utcnow()
+ image1 = {
+ 'id': 1,
+ 'name': 'image 1',
+ 'created_at': now.isoformat(),
+ 'updated_at': now.isoformat(),
+ 'deleted_at': None,
+ }
+ image2 = {
+ 'id': 2,
+ 'name': 'image 2',
+ 'deleted_at': now.isoformat(),
+ }
+ self.client.images = {1: image1, 2: image2}
+ i1, i2 = self.service.detail({})
+ self.assertEqual(i1['created_at'], now)
+ self.assertEqual(i1['updated_at'], now)
+ self.assertEqual(i1['deleted_at'], None)
+ self.assertEqual(i2['deleted_at'], now)
+
+ def test_get_handles_timestamps(self):
+ now = datetime.datetime.utcnow()
+ self.client.images = {'abcd': {
+ 'id': 'abcd',
+ 'name': 'nifty image',
+ 'created_at': now.isoformat(),
+ 'updated_at': now.isoformat(),
+ 'deleted_at': now.isoformat(),
+ }}
+ actual = self.service.get({}, 'abcd', NullWriter())
+ for attr in ('created_at', 'updated_at', 'deleted_at'):
+ self.assertEqual(actual[attr], now)
+
+ def test_get_handles_deleted_at_none(self):
+ self.client.images = {'abcd': {'deleted_at': None}}
+ actual = self.service.get({}, 'abcd', NullWriter())
+ self.assertEqual(actual['deleted_at'], None)
+
+ def test_create_handles_timestamps(self):
+ now = datetime.datetime.utcnow()
+ self.client.add_response = {
+ 'id': 'abcd',
+ 'name': 'blah',
+ 'created_at': now.isoformat(),
+ 'updated_at': now.isoformat(),
+ 'deleted_at': now.isoformat(),
+ }
+ actual = self.service.create({}, {})
+ for attr in ('created_at', 'updated_at', 'deleted_at'):
+ self.assertEqual(actual[attr], now)
+
+ def test_create_handles_deleted_at_none(self):
+ self.client.add_response = {
+ 'id': 'abcd',
+ 'name': 'blah',
+ 'deleted_at': None,
+ }
+ actual = self.service.create({}, {})
+ self.assertEqual(actual['deleted_at'], None)
+
+ def test_update_handles_timestamps(self):
+ now = datetime.datetime.utcnow()
+ self.client.update_response = {
+ 'id': 'abcd',
+ 'name': 'blah',
+ 'created_at': now.isoformat(),
+ 'updated_at': now.isoformat(),
+ 'deleted_at': now.isoformat(),
+ }
+ actual = self.service.update({}, 'dummy_id', {})
+ for attr in ('created_at', 'updated_at', 'deleted_at'):
+ self.assertEqual(actual[attr], now)
+
+ def test_create_handles_deleted_at_none(self):
+ self.client.update_response = {
+ 'id': 'abcd',
+ 'name': 'blah',
+ 'deleted_at': None,
+ }
+ actual = self.service.update({}, 'dummy_id', {})
+ self.assertEqual(actual['deleted_at'], None)
diff --git a/nova/tests/test_compute.py b/nova/tests/test_compute.py
index db65f4516..14ff8842b 100644
--- a/nova/tests/test_compute.py
+++ b/nova/tests/test_compute.py
@@ -82,6 +82,21 @@ class ComputeTestCase(test.TestCase):
inst.update(params)
return db.instance_create(self.context, inst)['id']
+ def _create_instance_type(self, params={}):
+ """Create a test instance"""
+ context = self.context.elevated()
+ inst = {}
+ inst['name'] = 'm1.small'
+ inst['memory_mb'] = '1024'
+ inst['vcpus'] = '1'
+ inst['local_gb'] = '20'
+ inst['flavorid'] = '1'
+ inst['swap'] = '2048'
+ inst['rxtx_quota'] = 100
+ inst['rxtx_cap'] = 200
+ inst.update(params)
+ return db.instance_type_create(context, inst)['id']
+
def _create_group(self):
values = {'name': 'testgroup',
'description': 'testgroup',
@@ -299,15 +314,53 @@ class ComputeTestCase(test.TestCase):
"""Ensure instance can be migrated/resized"""
instance_id = self._create_instance()
context = self.context.elevated()
+
self.compute.run_instance(self.context, instance_id)
db.instance_update(self.context, instance_id, {'host': 'foo'})
- self.compute.prep_resize(context, instance_id)
+ self.compute.prep_resize(context, instance_id, 1)
migration_ref = db.migration_get_by_instance_and_status(context,
instance_id, 'pre-migrating')
self.compute.resize_instance(context, instance_id,
migration_ref['id'])
self.compute.terminate_instance(context, instance_id)
+ def test_resize_invalid_flavor_fails(self):
+ """Ensure invalid flavors raise"""
+ instance_id = self._create_instance()
+ context = self.context.elevated()
+ self.compute.run_instance(self.context, instance_id)
+
+ self.assertRaises(exception.NotFound, self.compute_api.resize,
+ context, instance_id, 200)
+
+ self.compute.terminate_instance(context, instance_id)
+
+ def test_resize_down_fails(self):
+ """Ensure resizing down raises and fails"""
+ context = self.context.elevated()
+ instance_id = self._create_instance()
+
+ self.compute.run_instance(self.context, instance_id)
+ db.instance_update(self.context, instance_id,
+ {'instance_type': 'm1.xlarge'})
+
+ self.assertRaises(exception.ApiError, self.compute_api.resize,
+ context, instance_id, 1)
+
+ self.compute.terminate_instance(context, instance_id)
+
+ def test_resize_same_size_fails(self):
+ """Ensure invalid flavors raise"""
+ context = self.context.elevated()
+ instance_id = self._create_instance()
+
+ self.compute.run_instance(self.context, instance_id)
+
+ self.assertRaises(exception.ApiError, self.compute_api.resize,
+ context, instance_id, 1)
+
+ self.compute.terminate_instance(context, instance_id)
+
def test_get_by_flavor_id(self):
type = instance_types.get_by_flavor_id(1)
self.assertEqual(type, 'm1.tiny')
@@ -318,10 +371,8 @@ class ComputeTestCase(test.TestCase):
instance_id = self._create_instance()
self.compute.run_instance(self.context, instance_id)
self.assertRaises(exception.Error, self.compute.prep_resize,
- self.context, instance_id)
+ self.context, instance_id, 1)
self.compute.terminate_instance(self.context, instance_id)
- type = instance_types.get_by_flavor_id("1")
- self.assertEqual(type, 'm1.tiny')
def _setup_other_managers(self):
self.volume_manager = utils.import_object(FLAGS.volume_manager)
diff --git a/nova/tests/test_localization.py b/nova/tests/test_localization.py
index 393d71038..a25809a79 100644
--- a/nova/tests/test_localization.py
+++ b/nova/tests/test_localization.py
@@ -21,9 +21,10 @@ import sys
import unittest
import nova
+from nova import test
-class LocalizationTestCase(unittest.TestCase):
+class LocalizationTestCase(test.TestCase):
def test_multiple_positional_format_placeholders(self):
pat = re.compile("\W_\(")
single_pat = re.compile("\W%\W")
diff --git a/nova/tests/test_misc.py b/nova/tests/test_misc.py
index 1fbaf304f..4e17e1ce0 100644
--- a/nova/tests/test_misc.py
+++ b/nova/tests/test_misc.py
@@ -18,8 +18,12 @@ import errno
import os
import select
+from eventlet import greenpool
+from eventlet import greenthread
+
from nova import test
-from nova.utils import parse_mailmap, str_dict_replace, synchronized
+from nova import utils
+from nova.utils import parse_mailmap, str_dict_replace
class ProjectTestCase(test.TestCase):
@@ -63,7 +67,7 @@ class ProjectTestCase(test.TestCase):
class LockTestCase(test.TestCase):
def test_synchronized_wrapped_function_metadata(self):
- @synchronized('whatever')
+ @utils.synchronized('whatever')
def foo():
"""Bar"""
pass
@@ -72,11 +76,42 @@ class LockTestCase(test.TestCase):
self.assertEquals(foo.__name__, 'foo', "Wrapped function's name "
"got mangled")
- def test_synchronized(self):
+ def test_synchronized_internally(self):
+ """We can lock across multiple green threads"""
+ saved_sem_num = len(utils._semaphores)
+ seen_threads = list()
+
+ @utils.synchronized('testlock2', external=False)
+ def f(id):
+ for x in range(10):
+ seen_threads.append(id)
+ greenthread.sleep(0)
+
+ threads = []
+ pool = greenpool.GreenPool(10)
+ for i in range(10):
+ threads.append(pool.spawn(f, i))
+
+ for thread in threads:
+ thread.wait()
+
+ self.assertEquals(len(seen_threads), 100)
+ # Looking at the seen threads, split it into chunks of 10, and verify
+ # that the last 9 match the first in each chunk.
+ for i in range(10):
+ for j in range(9):
+ self.assertEquals(seen_threads[i * 10],
+ seen_threads[i * 10 + 1 + j])
+
+ self.assertEqual(saved_sem_num, len(utils._semaphores),
+ "Semaphore leak detected")
+
+ def test_synchronized_externally(self):
+ """We can lock across multiple processes"""
rpipe1, wpipe1 = os.pipe()
rpipe2, wpipe2 = os.pipe()
- @synchronized('testlock')
+ @utils.synchronized('testlock1', external=True)
def f(rpipe, wpipe):
try:
os.write(wpipe, "foo")
diff --git a/nova/tests/test_virt.py b/nova/tests/test_virt.py
index b214f5ce7..6bafac39f 100644
--- a/nova/tests/test_virt.py
+++ b/nova/tests/test_virt.py
@@ -77,13 +77,11 @@ class CacheConcurrencyTestCase(test.TestCase):
eventlet.sleep(0)
try:
self.assertFalse(done2.ready())
- self.assertTrue('fname' in conn._image_sems)
finally:
wait1.send()
done1.wait()
eventlet.sleep(0)
self.assertTrue(done2.ready())
- self.assertFalse('fname' in conn._image_sems)
def test_different_fname_concurrency(self):
"""Ensures that two different fname caches are concurrent"""
diff --git a/nova/tests/xenapi/stubs.py b/nova/tests/xenapi/stubs.py
index 70d46a1fb..7f9706a3d 100644
--- a/nova/tests/xenapi/stubs.py
+++ b/nova/tests/xenapi/stubs.py
@@ -228,6 +228,9 @@ class FakeSessionForMigrationTests(fake.SessionBase):
def VDI_get_by_uuid(*args):
return 'hurr'
+ def VDI_resize_online(*args):
+ pass
+
def VM_start(self, _1, ref, _2, _3):
vm = fake.get_record('VM', ref)
if vm['power_state'] != 'Halted':
@@ -240,7 +243,7 @@ class FakeSessionForMigrationTests(fake.SessionBase):
def stub_out_migration_methods(stubs):
def fake_get_snapshot(self, instance):
- return 'foo', 'bar'
+ return 'vm_ref', dict(image='foo', snap='bar')
@classmethod
def fake_get_vdi(cls, session, vm_ref):
@@ -249,7 +252,7 @@ def stub_out_migration_methods(stubs):
vdi_rec = session.get_xenapi().VDI.get_record(vdi_ref)
return vdi_ref, {'uuid': vdi_rec['uuid'], }
- def fake_shutdown(self, inst, vm, method='clean'):
+ def fake_shutdown(self, inst, vm, hard=True):
pass
@classmethod
diff --git a/nova/utils.py b/nova/utils.py
index e624c3f11..352c5d9f9 100644
--- a/nova/utils.py
+++ b/nova/utils.py
@@ -41,6 +41,7 @@ from xml.sax import saxutils
from eventlet import event
from eventlet import greenthread
+from eventlet import semaphore
from eventlet.green import subprocess
None
from nova import exception
@@ -531,17 +532,76 @@ def loads(s):
return json.loads(s)
-def synchronized(name):
+_semaphores = {}
+
+
+class _NoopContextManager(object):
+ def __enter__(self):
+ pass
+
+ def __exit__(self, exc_type, exc_val, exc_tb):
+ pass
+
+
+def synchronized(name, external=False):
+ """Synchronization decorator
+
+ Decorating a method like so:
+ @synchronized('mylock')
+ def foo(self, *args):
+ ...
+
+ ensures that only one thread will execute the bar method at a time.
+
+ Different methods can share the same lock:
+ @synchronized('mylock')
+ def foo(self, *args):
+ ...
+
+ @synchronized('mylock')
+ def bar(self, *args):
+ ...
+
+ This way only one of either foo or bar can be executing at a time.
+
+ The external keyword argument denotes whether this lock should work across
+ multiple processes. This means that if two different workers both run a
+ a method decorated with @synchronized('mylock', external=True), only one
+ of them will execute at a time.
+ """
+
def wrap(f):
@functools.wraps(f)
def inner(*args, **kwargs):
- LOG.debug(_("Attempting to grab %(lock)s for method "
- "%(method)s..." % {"lock": name,
+ # NOTE(soren): If we ever go natively threaded, this will be racy.
+ # See http://stackoverflow.com/questions/5390569/dyn\
+ # amically-allocating-and-destroying-mutexes
+ if name not in _semaphores:
+ _semaphores[name] = semaphore.Semaphore()
+ sem = _semaphores[name]
+ LOG.debug(_('Attempting to grab semaphore "%(lock)s" for method '
+ '"%(method)s"...' % {"lock": name,
"method": f.__name__}))
- lock = lockfile.FileLock(os.path.join(FLAGS.lock_path,
- 'nova-%s.lock' % name))
- with lock:
- return f(*args, **kwargs)
+ with sem:
+ if external:
+ LOG.debug(_('Attempting to grab file lock "%(lock)s" for '
+ 'method "%(method)s"...' %
+ {"lock": name, "method": f.__name__}))
+ lock_file_path = os.path.join(FLAGS.lock_path,
+ 'nova-%s.lock' % name)
+ lock = lockfile.FileLock(lock_file_path)
+ else:
+ lock = _NoopContextManager()
+
+ with lock:
+ retval = f(*args, **kwargs)
+
+ # If no-one else is waiting for it, delete it.
+ # See note about possible raciness above.
+ if not sem.balance < 1:
+ del _semaphores[name]
+
+ return retval
return inner
return wrap
diff --git a/nova/virt/libvirt_conn.py b/nova/virt/libvirt_conn.py
index ddc525e06..78395f5e8 100644
--- a/nova/virt/libvirt_conn.py
+++ b/nova/virt/libvirt_conn.py
@@ -48,7 +48,7 @@ from xml.dom import minidom
from eventlet import greenthread
from eventlet import tpool
-from eventlet import semaphore
+
import IPy
from nova import context
@@ -581,13 +581,12 @@ class LibvirtConnection(driver.ComputeDriver):
os.mkdir(base_dir)
base = os.path.join(base_dir, fname)
- if fname not in LibvirtConnection._image_sems:
- LibvirtConnection._image_sems[fname] = semaphore.Semaphore()
- with LibvirtConnection._image_sems[fname]:
+ @utils.synchronized(fname)
+ def call_if_not_exists(base, fn, *args, **kwargs):
if not os.path.exists(base):
fn(target=base, *args, **kwargs)
- if not LibvirtConnection._image_sems[fname].locked():
- del LibvirtConnection._image_sems[fname]
+
+ call_if_not_exists(base, fn, *args, **kwargs)
if cow:
utils.execute('qemu-img', 'create', '-f', 'qcow2', '-o',
@@ -1805,15 +1804,15 @@ class IptablesFirewallDriver(FirewallDriver):
pass
def refresh_security_group_rules(self, security_group):
- # We use the semaphore to make sure noone applies the rule set
- # after we've yanked the existing rules but before we've put in
- # the new ones.
- with self.iptables.semaphore:
- for instance in self.instances.values():
- self.remove_filters_for_instance(instance)
- self.add_filters_for_instance(instance)
+ self.do_refresh_security_group_rules(security_group)
self.iptables.apply()
+ @utils.synchronized('iptables', external=True)
+ def do_refresh_security_group_rules(self, security_group):
+ for instance in self.instances.values():
+ self.remove_filters_for_instance(instance)
+ self.add_filters_for_instance(instance)
+
def _security_group_chain_name(self, security_group_id):
return 'nova-sg-%s' % (security_group_id,)
diff --git a/nova/virt/xenapi/vmops.py b/nova/virt/xenapi/vmops.py
index 3fd98be67..80a5365c6 100644
--- a/nova/virt/xenapi/vmops.py
+++ b/nova/virt/xenapi/vmops.py
@@ -82,6 +82,17 @@ class VMOps(object):
instance_infos.append(instance_info)
return instance_infos
+ def revert_resize(self, instance):
+ vm_ref = VMHelper.lookup(self._session, instance.name)
+ self._start(instance, vm_ref)
+
+ def finish_resize(self, instance, disk_info):
+ vdi_uuid = self.link_disks(instance, disk_info['base_copy'],
+ disk_info['cow'])
+ vm_ref = self._create_vm(instance, vdi_uuid)
+ self.resize_instance(instance, vdi_uuid)
+ self._spawn(instance, vm_ref)
+
def _start(self, instance, vm_ref=None):
"""Power on a VM instance"""
if not vm_ref:
@@ -92,7 +103,7 @@ class VMOps(object):
LOG.debug(_("Starting instance %s"), instance.name)
self._session.call_xenapi('VM.start', vm_ref, False, False)
- def create_disk(self, instance):
+ def _create_disk(self, instance):
user = AuthManager().get_user(instance.user_id)
project = AuthManager().get_project(instance.project_id)
disk_image_type = VMHelper.determine_disk_image_type(instance)
@@ -101,10 +112,11 @@ class VMOps(object):
return vdi_uuid
def spawn(self, instance, network_info=None):
- vdi_uuid = self.create_disk(instance)
- self._spawn_with_disk(instance, vdi_uuid, network_info)
+ vdi_uuid = self._create_disk(instance)
+ vm_ref = self._create_vm(instance, vdi_uuid, network_info)
+ self._spawn(instance, vm_ref)
- def _spawn_with_disk(self, instance, vdi_uuid, network_info=None):
+ def _create_vm(self, instance, vdi_uuid, network_info=None):
"""Create VM instance"""
instance_name = instance.name
vm_ref = VMHelper.lookup(self._session, instance_name)
@@ -147,16 +159,19 @@ class VMOps(object):
VMHelper.create_vbd(session=self._session, vm_ref=vm_ref,
vdi_ref=vdi_ref, userdevice=0, bootable=True)
- # inject_network_info and create vifs
# TODO(tr3buchet) - check to make sure we have network info, otherwise
# create it now. This goes away once nova-multi-nic hits.
if network_info is None:
network_info = self._get_network_info(instance)
self.create_vifs(vm_ref, network_info)
self.inject_network_info(instance, vm_ref, network_info)
+ return vm_ref
+ def _spawn(self, instance, vm_ref):
+ """Spawn a new instance"""
LOG.debug(_('Starting VM %s...'), vm_ref)
self._start(instance, vm_ref)
+ instance_name = instance.name
LOG.info(_('Spawning VM %(instance_name)s created %(vm_ref)s.')
% locals())
@@ -329,7 +344,7 @@ class VMOps(object):
try:
# transfer the base copy
template_vm_ref, template_vdi_uuids = self._get_snapshot(instance)
- base_copy_uuid = template_vdi_uuids[1]
+ base_copy_uuid = template_vdi_uuids['image']
vdi_ref, vm_vdi_rec = \
VMHelper.get_vdi_for_vm_safely(self._session, vm_ref)
cow_uuid = vm_vdi_rec['uuid']
@@ -344,7 +359,7 @@ class VMOps(object):
self._session.wait_for_task(task, instance.id)
# Now power down the instance and transfer the COW VHD
- self._shutdown(instance, vm_ref, method='clean')
+ self._shutdown(instance, vm_ref, hard=False)
params = {'host': dest,
'vdi_uuid': cow_uuid,
@@ -364,7 +379,7 @@ class VMOps(object):
# sensible so we don't need to blindly pass around dictionaries
return {'base_copy': base_copy_uuid, 'cow': cow_uuid}
- def attach_disk(self, instance, base_copy_uuid, cow_uuid):
+ def link_disks(self, instance, base_copy_uuid, cow_uuid):
"""Links the base copy VHD to the COW via the XAPI plugin"""
vm_ref = VMHelper.lookup(self._session, instance.name)
new_base_copy_uuid = str(uuid.uuid4())
@@ -385,9 +400,19 @@ class VMOps(object):
return new_cow_uuid
- def resize(self, instance, flavor):
+ def resize_instance(self, instance, vdi_uuid):
"""Resize a running instance by changing it's RAM and disk size """
- raise NotImplementedError()
+ #TODO(mdietz): this will need to be adjusted for swap later
+ #The new disk size must be in bytes
+
+ new_disk_size = str(instance.local_gb * 1024 * 1024 * 1024)
+ instance_name = instance.name
+ instance_local_gb = instance.local_gb
+ LOG.debug(_("Resizing VDI %(vdi_uuid)s for instance %(instance_name)s."
+ " Expanding to %(instance_local_gb)d GB") % locals())
+ vdi_ref = self._session.call_xenapi('VDI.get_by_uuid', vdi_uuid)
+ self._session.call_xenapi('VDI.resize_online', vdi_ref, new_disk_size)
+ LOG.debug(_("Resize instance %s complete") % (instance.name))
def reboot(self, instance):
"""Reboot VM instance"""
@@ -462,8 +487,9 @@ class VMOps(object):
"""Shutdown an instance"""
state = self.get_info(instance['name'])['state']
if state == power_state.SHUTDOWN:
- LOG.warn(_("VM %(vm)s already halted, skipping shutdown...") %
- locals())
+ instance_name = instance.name
+ LOG.warn(_("VM %(instance_name)s already halted,"
+ "skipping shutdown...") % locals())
return
instance_id = instance.id
@@ -742,8 +768,9 @@ class VMOps(object):
'mac': instance.mac_address,
'rxtx_cap': flavor['rxtx_cap'],
'dns': [network['dns']],
- 'ips': [ip_dict(ip) for ip in network_IPs],
- 'ip6s': [ip6_dict(ip) for ip in network_IPs]}
+ 'ips': [ip_dict(ip) for ip in network_IPs]}
+ if network['cidr_v6']:
+ info['ip6s'] = [ip6_dict(ip) for ip in network_IPs]
network_info.append((network, info))
return network_info
diff --git a/nova/virt/xenapi_conn.py b/nova/virt/xenapi_conn.py
index b5bff6c26..5cdef4a3b 100644
--- a/nova/virt/xenapi_conn.py
+++ b/nova/virt/xenapi_conn.py
@@ -169,20 +169,18 @@ class XenAPIConnection(driver.ComputeDriver):
"""Create VM instance"""
self._vmops.spawn(instance)
+ def revert_resize(self, instance):
+ """Reverts a resize, powering back on the instance"""
+ self._vmops.revert_resize(instance)
+
def finish_resize(self, instance, disk_info):
"""Completes a resize, turning on the migrated instance"""
- vdi_uuid = self._vmops.attach_disk(instance, disk_info['base_copy'],
- disk_info['cow'])
- self._vmops._spawn_with_disk(instance, vdi_uuid)
+ self._vmops.finish_resize(instance, disk_info)
def snapshot(self, instance, image_id):
""" Create snapshot from a running VM instance """
self._vmops.snapshot(instance, image_id)
- def resize(self, instance, flavor):
- """Resize a VM instance"""
- raise NotImplementedError()
-
def reboot(self, instance):
"""Reboot VM instance"""
self._vmops.reboot(instance)
diff --git a/plugins/xenserver/xenapi/etc/xapi.d/plugins/migration b/plugins/xenserver/xenapi/etc/xapi.d/plugins/migration
index 4aa89863a..75c653408 100644
--- a/plugins/xenserver/xenapi/etc/xapi.d/plugins/migration
+++ b/plugins/xenserver/xenapi/etc/xapi.d/plugins/migration
@@ -22,6 +22,7 @@ XenAPI Plugin for transfering data between host nodes
import os
import os.path
import pickle
+import shlex
import shutil
import subprocess
@@ -97,7 +98,7 @@ def transfer_vhd(session, args):
logging.debug("Preparing to transmit %s to %s" % (source_path,
dest_path))
- ssh_cmd = 'ssh -o StrictHostKeyChecking=no'
+ ssh_cmd = '\"ssh -o StrictHostKeyChecking=no\"'
rsync_args = shlex.split('nohup /usr/bin/rsync -av --progress -e %s %s %s'
% (ssh_cmd, source_path, dest_path))