merge trunk
This commit is contained in:
@@ -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')
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -437,25 +437,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:
|
||||
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 prep_resize(self, context, instance_id):
|
||||
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, 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()
|
||||
@@ -464,12 +480,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,
|
||||
@@ -495,8 +516,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,
|
||||
@@ -517,7 +536,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,
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
@@ -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))
|
||||
|
||||
@@ -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):
|
||||
"""
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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)]
|
||||
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)
|
||||
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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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 self.IMAGE_FIXTURES:
|
||||
expected = {
|
||||
'id': abs(hash(image['imageId'])),
|
||||
'name': image['name'],
|
||||
'status': 'active',
|
||||
}
|
||||
self.assertTrue(expected in res_dict['images'])
|
||||
|
||||
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))
|
||||
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'])
|
||||
|
||||
@@ -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))
|
||||
|
||||
|
||||
16
nova/tests/image/__init__.py
Normal file
16
nova/tests/image/__init__.py
Normal file
@@ -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.
|
||||
188
nova/tests/image/test_glance.py
Normal file
188
nova/tests/image/test_glance.py
Normal file
@@ -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)
|
||||
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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"""
|
||||
|
||||
@@ -232,6 +232,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':
|
||||
@@ -244,7 +247,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):
|
||||
@@ -253,7 +256,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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -554,13 +554,12 @@ class LibvirtConnection(object):
|
||||
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',
|
||||
@@ -1756,15 +1755,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,)
|
||||
|
||||
|
||||
@@ -64,6 +64,17 @@ class VMOps(object):
|
||||
vm_refs.append(vm_rec["name_label"])
|
||||
return vm_refs
|
||||
|
||||
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:
|
||||
@@ -74,7 +85,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)
|
||||
@@ -83,10 +94,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)
|
||||
@@ -133,16 +145,19 @@ class VMOps(object):
|
||||
if FLAGS.xenapi_inject_image:
|
||||
VMHelper.preconfigure_instance(self._session, instance, vdi_ref)
|
||||
|
||||
# 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())
|
||||
|
||||
@@ -306,7 +321,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']
|
||||
@@ -321,7 +336,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,
|
||||
@@ -341,7 +356,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())
|
||||
@@ -362,9 +377,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"""
|
||||
@@ -439,8 +464,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
|
||||
@@ -720,8 +746,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
|
||||
|
||||
|
||||
@@ -178,20 +178,18 @@ class XenAPIConnection(object):
|
||||
"""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)
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -32,7 +32,6 @@ SUITE_NAMES = '[image, instance, volume]'
|
||||
FLAGS = flags.FLAGS
|
||||
flags.DEFINE_string('suite', None, 'Specific test suite to run ' + SUITE_NAMES)
|
||||
flags.DEFINE_integer('ssh_tries', 3, 'Numer of times to try ssh')
|
||||
boto_v6 = None
|
||||
|
||||
|
||||
class SmokeTestCase(unittest.TestCase):
|
||||
@@ -183,6 +182,9 @@ class SmokeTestCase(unittest.TestCase):
|
||||
|
||||
|
||||
TEST_DATA = {}
|
||||
if FLAGS.use_ipv6:
|
||||
global boto_v6
|
||||
boto_v6 = __import__('boto_v6')
|
||||
|
||||
|
||||
class UserSmokeTestCase(SmokeTestCase):
|
||||
|
||||
Reference in New Issue
Block a user