merged trunk

This commit is contained in:
Trey Morris 2011-06-29 16:44:42 -05:00
commit 51f64e0073
20 changed files with 704 additions and 82 deletions

116
bin/instance-usage-audit Executable file
View File

@ -0,0 +1,116 @@
#!/usr/bin/env python
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright (c) 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.
"""Cron script to generate usage notifications for instances neither created
nor destroyed in a given time period.
Together with the notifications generated by compute on instance
create/delete/resize, over that ime period, this allows an external
system consuming usage notification feeds to calculate instance usage
for each tenant.
Time periods are specified like so:
<number>[mdy]
1m = previous month. If the script is run April 1, it will generate usages
for March 1 thry March 31.
3m = 3 previous months.
90d = previous 90 days.
1y = previous year. If run on Jan 1, it generates usages for
Jan 1 thru Dec 31 of the previous year.
"""
import datetime
import gettext
import os
import sys
import time
# If ../nova/__init__.py exists, add ../ to Python search path, so that
# it will override what happens to be installed in /usr/(local/)lib/python...
POSSIBLE_TOPDIR = os.path.normpath(os.path.join(os.path.abspath(sys.argv[0]),
os.pardir,
os.pardir))
if os.path.exists(os.path.join(POSSIBLE_TOPDIR, 'nova', '__init__.py')):
sys.path.insert(0, POSSIBLE_TOPDIR)
gettext.install('nova', unicode=1)
from nova import context
from nova import db
from nova import exception
from nova import flags
from nova import log as logging
from nova import utils
from nova.notifier import api as notifier_api
FLAGS = flags.FLAGS
flags.DEFINE_string('instance_usage_audit_period', '1m',
'time period to generate instance usages for.')
def time_period(period):
today = datetime.date.today()
unit = period[-1]
if unit not in 'mdy':
raise ValueError('Time period must be m, d, or y')
n = int(period[:-1])
if unit == 'm':
year = today.year - (n // 12)
n = n % 12
if n >= today.month:
year -= 1
month = 12 + (today.month - n)
else:
month = today.month - n
begin = datetime.datetime(day=1, month=month, year=year)
end = datetime.datetime(day=1, month=today.month, year=today.year)
elif unit == 'y':
begin = datetime.datetime(day=1, month=1, year=today.year - n)
end = datetime.datetime(day=1, month=1, year=today.year)
elif unit == 'd':
b = today - datetime.timedelta(days=n)
begin = datetime.datetime(day=b.day, month=b.month, year=b.year)
end = datetime.datetime(day=today.day,
month=today.month,
year=today.year)
return (begin, end)
if __name__ == '__main__':
utils.default_flagfile()
flags.FLAGS(sys.argv)
logging.setup()
begin, end = time_period(FLAGS.instance_usage_audit_period)
print "Creating usages for %s until %s" % (str(begin), str(end))
instances = db.instance_get_active_by_window(context.get_admin_context(),
begin,
end)
print "%s instances" % len(instances)
for instance_ref in instances:
usage_info = utils.usage_from_instance(instance_ref,
audit_period_begining=str(begin),
audit_period_ending=str(end))
notifier_api.notify('compute.%s' % FLAGS.host,
'compute.instance.exists',
notifier_api.INFO,
usage_info)

View File

@ -23,8 +23,14 @@ Starts both the EC2 and OpenStack APIs in separate processes.
""" """
import os
import sys import sys
possible_topdir = os.path.normpath(os.path.join(os.path.abspath(
sys.argv[0]), os.pardir, os.pardir))
if os.path.exists(os.path.join(possible_topdir, "nova", "__init__.py")):
sys.path.insert(0, possible_topdir)
import nova.service import nova.service
import nova.utils import nova.utils

View File

@ -644,7 +644,7 @@ class VmCommands(object):
:param host: show all instance on specified host. :param host: show all instance on specified host.
:param instance: show specificed instance. :param instance: show specificed instance.
""" """
print "%-10s %-15s %-10s %-10s %-19s %-12s %-12s %-12s" \ print "%-10s %-15s %-10s %-10s %-26s %-9s %-9s %-9s" \
" %-10s %-10s %-10s %-5s" % ( " %-10s %-10s %-10s %-5s" % (
_('instance'), _('instance'),
_('node'), _('node'),
@ -666,14 +666,14 @@ class VmCommands(object):
context.get_admin_context(), host) context.get_admin_context(), host)
for instance in instances: for instance in instances:
print "%-10s %-15s %-10s %-10s %-19s %-12s %-12s %-12s" \ print "%-10s %-15s %-10s %-10s %-26s %-9s %-9s %-9s" \
" %-10s %-10s %-10s %-5d" % ( " %-10s %-10s %-10s %-5d" % (
instance['hostname'], instance['hostname'],
instance['host'], instance['host'],
instance['instance_type'], instance['instance_type'].name,
instance['state_description'], instance['state_description'],
instance['launched_at'], instance['launched_at'],
instance['image_id'], instance['image_ref'],
instance['kernel_id'], instance['kernel_id'],
instance['ramdisk_id'], instance['ramdisk_id'],
instance['project_id'], instance['project_id'],
@ -905,7 +905,7 @@ class InstanceTypeCommands(object):
try: try:
instance_types.create(name, memory, vcpus, local_gb, instance_types.create(name, memory, vcpus, local_gb,
flavorid, swap, rxtx_quota, rxtx_cap) flavorid, swap, rxtx_quota, rxtx_cap)
except exception.InvalidInput: except exception.InvalidInput, e:
print "Must supply valid parameters to create instance_type" print "Must supply valid parameters to create instance_type"
print e print e
sys.exit(1) sys.exit(1)

View File

@ -45,23 +45,20 @@ def get_pagination_params(request):
exc.HTTPBadRequest() exceptions to be raised. exc.HTTPBadRequest() exceptions to be raised.
""" """
params = {}
for param in ['marker', 'limit']:
if not param in request.GET:
continue
try: try:
marker = int(request.GET.get('marker', 0)) params[param] = int(request.GET[param])
except ValueError: except ValueError:
raise webob.exc.HTTPBadRequest(_('marker param must be an integer')) msg = _('%s param must be an integer') % param
raise webob.exc.HTTPBadRequest(msg)
if params[param] < 0:
msg = _('%s param must be positive') % param
raise webob.exc.HTTPBadRequest(msg)
try: return params
limit = int(request.GET.get('limit', 0))
except ValueError:
raise webob.exc.HTTPBadRequest(_('limit param must be an integer'))
if limit < 0:
raise webob.exc.HTTPBadRequest(_('limit param must be positive'))
if marker < 0:
raise webob.exc.HTTPBadRequest(_('marker param must be positive'))
return(marker, limit)
def limited(items, request, max_limit=FLAGS.osapi_max_limit): def limited(items, request, max_limit=FLAGS.osapi_max_limit):
@ -100,10 +97,10 @@ def limited(items, request, max_limit=FLAGS.osapi_max_limit):
def limited_by_marker(items, request, max_limit=FLAGS.osapi_max_limit): def limited_by_marker(items, request, max_limit=FLAGS.osapi_max_limit):
"""Return a slice of items according to the requested marker and limit.""" """Return a slice of items according to the requested marker and limit."""
(marker, limit) = get_pagination_params(request) params = get_pagination_params(request)
if limit == 0: limit = params.get('limit', max_limit)
limit = max_limit marker = params.get('marker')
limit = min(max_limit, limit) limit = min(max_limit, limit)
start_index = 0 start_index = 0

View File

@ -90,31 +90,67 @@ class Controller(object):
return webob.exc.HTTPNoContent() return webob.exc.HTTPNoContent()
def create(self, req, body): def create(self, req, body):
"""Snapshot a server instance and save the image. """Snapshot or backup a server instance and save the image.
Images now have an `image_type` associated with them, which can be
'snapshot' or the backup type, like 'daily' or 'weekly'.
If the image_type is backup-like, then the rotation factor can be
included and that will cause the oldest backups that exceed the
rotation factor to be deleted.
:param req: `wsgi.Request` object :param req: `wsgi.Request` object
""" """
def get_param(param):
try:
return body["image"][param]
except KeyError:
raise webob.exc.HTTPBadRequest(explanation="Missing required "
"param: %s" % param)
context = req.environ['nova.context'] context = req.environ['nova.context']
content_type = req.get_content_type() content_type = req.get_content_type()
if not body: if not body:
raise webob.exc.HTTPBadRequest() raise webob.exc.HTTPBadRequest()
image_type = body["image"].get("image_type", "snapshot")
try: try:
server_id = self._server_id_from_req(req, body) server_id = self._server_id_from_req(req, body)
image_name = body["image"]["name"]
except KeyError: except KeyError:
raise webob.exc.HTTPBadRequest() raise webob.exc.HTTPBadRequest()
image_name = get_param("name")
props = self._get_extra_properties(req, body) props = self._get_extra_properties(req, body)
image = self._compute_service.snapshot(context, server_id, if image_type == "snapshot":
image_name, props) image = self._compute_service.snapshot(
context, server_id, image_name,
extra_properties=props)
elif image_type == "backup":
# NOTE(sirp): Unlike snapshot, backup is not a customer facing
# API call; rather, it's used by the internal backup scheduler
if not FLAGS.allow_admin_api:
raise webob.exc.HTTPBadRequest(
explanation="Admin API Required")
backup_type = get_param("backup_type")
rotation = int(get_param("rotation"))
image = self._compute_service.backup(
context, server_id, image_name,
backup_type, rotation, extra_properties=props)
else:
LOG.error(_("Invalid image_type '%s' passed") % image_type)
raise webob.exc.HTTPBadRequest(explanation="Invalue image_type: "
"%s" % image_type)
return dict(image=self.get_builder(req).build(image, detail=True)) return dict(image=self.get_builder(req).build(image, detail=True))
def get_builder(self, request): def get_builder(self, request):
"""Indicates that you must use a Controller subclass.""" """Indicates that you must use a Controller subclass."""
raise NotImplementedError raise NotImplementedError()
def _server_id_from_req(self, req, data): def _server_id_from_req(self, req, data):
raise NotImplementedError() raise NotImplementedError()
@ -181,9 +217,9 @@ class ControllerV11(Controller):
""" """
context = req.environ['nova.context'] context = req.environ['nova.context']
filters = self._get_filters(req) filters = self._get_filters(req)
(marker, limit) = common.get_pagination_params(req) page_params = common.get_pagination_params(req)
images = self._image_service.index( images = self._image_service.index(context, filters=filters,
context, filters=filters, marker=marker, limit=limit) **page_params)
builder = self.get_builder(req).build builder = self.get_builder(req).build
return dict(images=[builder(image, detail=False) for image in images]) return dict(images=[builder(image, detail=False) for image in images])
@ -195,9 +231,9 @@ class ControllerV11(Controller):
""" """
context = req.environ['nova.context'] context = req.environ['nova.context']
filters = self._get_filters(req) filters = self._get_filters(req)
(marker, limit) = common.get_pagination_params(req) page_params = common.get_pagination_params(req)
images = self._image_service.detail( images = self._image_service.detail(context, filters=filters,
context, filters=filters, marker=marker, limit=limit) **page_params)
builder = self.get_builder(req).build builder = self.get_builder(req).build
return dict(images=[builder(image, detail=True) for image in images]) return dict(images=[builder(image, detail=True) for image in images])

View File

@ -693,19 +693,60 @@ class API(base.Base):
raise exception.Error(_("Unable to find host for Instance %s") raise exception.Error(_("Unable to find host for Instance %s")
% instance_id) % instance_id)
def backup(self, context, instance_id, name, backup_type, rotation,
extra_properties=None):
"""Backup the given instance
:param instance_id: nova.db.sqlalchemy.models.Instance.Id
:param name: name of the backup or snapshot
name = backup_type # daily backups are called 'daily'
:param rotation: int representing how many backups to keep around;
None if rotation shouldn't be used (as in the case of snapshots)
:param extra_properties: dict of extra image properties to include
"""
recv_meta = self._create_image(context, instance_id, name, 'backup',
backup_type=backup_type, rotation=rotation,
extra_properties=extra_properties)
return recv_meta
def snapshot(self, context, instance_id, name, extra_properties=None): def snapshot(self, context, instance_id, name, extra_properties=None):
"""Snapshot the given instance. """Snapshot the given instance.
:param instance_id: nova.db.sqlalchemy.models.Instance.Id
:param name: name of the backup or snapshot
:param extra_properties: dict of extra image properties to include
:returns: A dict containing image metadata :returns: A dict containing image metadata
""" """
properties = {'instance_id': str(instance_id), return self._create_image(context, instance_id, name, 'snapshot',
extra_properties=extra_properties)
def _create_image(self, context, instance_id, name, image_type,
backup_type=None, rotation=None, extra_properties=None):
"""Create snapshot or backup for an instance on this host.
:param context: security context
:param instance_id: nova.db.sqlalchemy.models.Instance.Id
:param name: string for name of the snapshot
:param image_type: snapshot | backup
:param backup_type: daily | weekly
:param rotation: int representing how many backups to keep around;
None if rotation shouldn't be used (as in the case of snapshots)
:param extra_properties: dict of extra image properties to include
"""
instance = db.api.instance_get(context, instance_id)
properties = {'instance_uuid': instance['uuid'],
'user_id': str(context.user_id), 'user_id': str(context.user_id),
'image_state': 'creating'} 'image_state': 'creating',
'image_type': image_type,
'backup_type': backup_type}
properties.update(extra_properties or {}) properties.update(extra_properties or {})
sent_meta = {'name': name, 'is_public': False, sent_meta = {'name': name, 'is_public': False,
'status': 'creating', 'properties': properties} 'status': 'creating', 'properties': properties}
recv_meta = self.image_service.create(context, sent_meta) recv_meta = self.image_service.create(context, sent_meta)
params = {'image_id': recv_meta['id']} params = {'image_id': recv_meta['id'], 'image_type': image_type,
'backup_type': backup_type, 'rotation': rotation}
self._cast_compute_message('snapshot_instance', context, instance_id, self._cast_compute_message('snapshot_instance', context, instance_id,
params=params) params=params)
return recv_meta return recv_meta

View File

@ -46,13 +46,16 @@ from eventlet import greenthread
from nova import exception from nova import exception
from nova import flags from nova import flags
import nova.image
from nova import log as logging from nova import log as logging
from nova import manager from nova import manager
from nova import network from nova import network
from nova import notifier
from nova import rpc from nova import rpc
from nova import utils from nova import utils
from nova import volume from nova import volume
from nova.compute import power_state from nova.compute import power_state
from nova.notifier import api as notifier_api
from nova.compute.utils import terminate_volumes from nova.compute.utils import terminate_volumes
from nova.virt import driver from nova.virt import driver
@ -315,6 +318,11 @@ class ComputeManager(manager.SchedulerDependentManager):
self._update_launched_at(context, instance_id) self._update_launched_at(context, instance_id)
self._update_state(context, instance_id) self._update_state(context, instance_id)
usage_info = utils.usage_from_instance(instance_ref)
notifier_api.notify('compute.%s' % self.host,
'compute.instance.create',
notifier_api.INFO,
usage_info)
except exception.InstanceNotFound: except exception.InstanceNotFound:
# FIXME(wwolf): We are just ignoring InstanceNotFound # FIXME(wwolf): We are just ignoring InstanceNotFound
# exceptions here in case the instance was immediately # exceptions here in case the instance was immediately
@ -364,9 +372,15 @@ class ComputeManager(manager.SchedulerDependentManager):
def terminate_instance(self, context, instance_id): def terminate_instance(self, context, instance_id):
"""Terminate an instance on this host.""" """Terminate an instance on this host."""
self._shutdown_instance(context, instance_id, 'Terminating') self._shutdown_instance(context, instance_id, 'Terminating')
instance_ref = self.db.instance_get(context.elevated(), instance_id)
# TODO(ja): should we keep it in a terminated state for a bit? # TODO(ja): should we keep it in a terminated state for a bit?
self.db.instance_destroy(context, instance_id) self.db.instance_destroy(context, instance_id)
usage_info = utils.usage_from_instance(instance_ref)
notifier_api.notify('compute.%s' % self.host,
'compute.instance.delete',
notifier_api.INFO,
usage_info)
@exception.wrap_exception @exception.wrap_exception
@checks_instance_lock @checks_instance_lock
@ -403,6 +417,12 @@ class ComputeManager(manager.SchedulerDependentManager):
self._update_image_ref(context, instance_id, image_ref) self._update_image_ref(context, instance_id, image_ref)
self._update_launched_at(context, instance_id) self._update_launched_at(context, instance_id)
self._update_state(context, instance_id) self._update_state(context, instance_id)
usage_info = utils.usage_from_instance(instance_ref,
image_ref=image_ref)
notifier_api.notify('compute.%s' % self.host,
'compute.instance.rebuild',
notifier_api.INFO,
usage_info)
@exception.wrap_exception @exception.wrap_exception
@checks_instance_lock @checks_instance_lock
@ -430,8 +450,19 @@ class ComputeManager(manager.SchedulerDependentManager):
self._update_state(context, instance_id) self._update_state(context, instance_id)
@exception.wrap_exception @exception.wrap_exception
def snapshot_instance(self, context, instance_id, image_id): def snapshot_instance(self, context, instance_id, image_id,
"""Snapshot an instance on this host.""" image_type='snapshot', backup_type=None,
rotation=None):
"""Snapshot an instance on this host.
:param context: security context
:param instance_id: nova.db.sqlalchemy.models.Instance.Id
:param image_id: glance.db.sqlalchemy.models.Image.Id
:param image_type: snapshot | backup
:param backup_type: daily | weekly
:param rotation: int representing how many backups to keep around;
None if rotation shouldn't be used (as in the case of snapshots)
"""
context = context.elevated() context = context.elevated()
instance_ref = self.db.instance_get(context, instance_id) instance_ref = self.db.instance_get(context, instance_id)
@ -451,6 +482,65 @@ class ComputeManager(manager.SchedulerDependentManager):
self.driver.snapshot(instance_ref, image_id) self.driver.snapshot(instance_ref, image_id)
if image_type == 'snapshot':
if rotation:
raise exception.ImageRotationNotAllowed()
elif image_type == 'backup':
if rotation:
instance_uuid = instance_ref['uuid']
self.rotate_backups(context, instance_uuid, backup_type,
rotation)
else:
raise exception.RotationRequiredForBackup()
else:
raise Exception(_('Image type not recognized %s') % image_type)
def rotate_backups(self, context, instance_uuid, backup_type, rotation):
"""Delete excess backups associated to an instance.
Instances are allowed a fixed number of backups (the rotation number);
this method deletes the oldest backups that exceed the rotation
threshold.
:param context: security context
:param instance_uuid: string representing uuid of instance
:param backup_type: daily | weekly
:param rotation: int representing how many backups to keep around;
None if rotation shouldn't be used (as in the case of snapshots)
"""
# NOTE(jk0): Eventually extract this out to the ImageService?
def fetch_images():
images = []
marker = None
while True:
batch = image_service.detail(context, filters=filters,
marker=marker, sort_key='created_at', sort_dir='desc')
if not batch:
break
images += batch
marker = batch[-1]['id']
return images
image_service = nova.image.get_default_image_service()
filters = {'property-image_type': 'backup',
'property-backup_type': backup_type,
'property-instance_uuid': instance_uuid}
images = fetch_images()
num_images = len(images)
LOG.debug(_("Found %(num_images)d images (rotation: %(rotation)d)"
% locals()))
if num_images > rotation:
# NOTE(sirp): this deletes all backups that exceed the rotation
# limit
excess = len(images) - rotation
LOG.debug(_("Rotating out %d backups" % excess))
for i in xrange(excess):
image = images.pop()
image_id = image['id']
LOG.debug(_("Deleting image %d" % image_id))
image_service.delete(context, image_id)
@exception.wrap_exception @exception.wrap_exception
@checks_instance_lock @checks_instance_lock
def set_admin_password(self, context, instance_id, new_pass=None): def set_admin_password(self, context, instance_id, new_pass=None):
@ -580,6 +670,11 @@ class ComputeManager(manager.SchedulerDependentManager):
context = context.elevated() context = context.elevated()
instance_ref = self.db.instance_get(context, instance_id) instance_ref = self.db.instance_get(context, instance_id)
self.driver.destroy(instance_ref) self.driver.destroy(instance_ref)
usage_info = utils.usage_from_instance(instance_ref)
notifier_api.notify('compute.%s' % self.host,
'compute.instance.resize.confirm',
notifier_api.INFO,
usage_info)
@exception.wrap_exception @exception.wrap_exception
@checks_instance_lock @checks_instance_lock
@ -627,6 +722,11 @@ class ComputeManager(manager.SchedulerDependentManager):
self.driver.revert_resize(instance_ref) self.driver.revert_resize(instance_ref)
self.db.migration_update(context, migration_id, self.db.migration_update(context, migration_id,
{'status': 'reverted'}) {'status': 'reverted'})
usage_info = utils.usage_from_instance(instance_ref)
notifier_api.notify('compute.%s' % self.host,
'compute.instance.resize.revert',
notifier_api.INFO,
usage_info)
@exception.wrap_exception @exception.wrap_exception
@checks_instance_lock @checks_instance_lock
@ -663,6 +763,13 @@ class ComputeManager(manager.SchedulerDependentManager):
'migration_id': migration_ref['id'], 'migration_id': migration_ref['id'],
'instance_id': instance_id, }, 'instance_id': instance_id, },
}) })
usage_info = utils.usage_from_instance(instance_ref,
new_instance_type=instance_type['name'],
new_instance_type_id=instance_type['id'])
notifier_api.notify('compute.%s' % self.host,
'compute.instance.resize.prep',
notifier_api.INFO,
usage_info)
@exception.wrap_exception @exception.wrap_exception
@checks_instance_lock @checks_instance_lock

View File

@ -494,6 +494,11 @@ def instance_get_all(context):
return IMPL.instance_get_all(context) return IMPL.instance_get_all(context)
def instance_get_active_by_window(context, begin, end=None):
"""Get instances active during a certain time window."""
return IMPL.instance_get_active_by_window(context, begin, end)
def instance_get_all_by_user(context, user_id): def instance_get_all_by_user(context, user_id):
"""Get all instances.""" """Get all instances."""
return IMPL.instance_get_all_by_user(context, user_id) return IMPL.instance_get_all_by_user(context, user_id)

View File

@ -1136,6 +1136,24 @@ def instance_get_all(context):
all() all()
@require_admin_context
def instance_get_active_by_window(context, begin, end=None):
"""Return instances that were continuously active over the given window"""
session = get_session()
query = session.query(models.Instance).\
options(joinedload_all('fixed_ip.floating_ips')).\
options(joinedload('security_groups')).\
options(joinedload_all('fixed_ip.network')).\
options(joinedload('instance_type')).\
filter(models.Instance.launched_at < begin)
if end:
query = query.filter(or_(models.Instance.terminated_at == None,
models.Instance.terminated_at > end))
else:
query = query.filter(models.Instance.terminated_at == None)
return query.all()
@require_admin_context @require_admin_context
def instance_get_all_by_user(context, user_id): def instance_get_all_by_user(context, user_id):
session = get_session() session = get_session()

View File

@ -591,6 +591,14 @@ class GlobalRoleNotAllowed(NotAllowed):
message = _("Unable to use global role %(role_id)s") message = _("Unable to use global role %(role_id)s")
class ImageRotationNotAllowed(NovaException):
message = _("Rotation is not allowed for snapshots")
class RotationRequiredForBackup(NovaException):
message = _("Rotation param is required for backup image_type")
#TODO(bcwaldon): EOL this exception! #TODO(bcwaldon): EOL this exception!
class Duplicate(NovaException): class Duplicate(NovaException):
pass pass

View File

@ -0,0 +1,28 @@
# Copyright 2011 OpenStack LLC.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import json
from nova import flags
from nova import log as logging
FLAGS = flags.FLAGS
NOTIFICATIONS = []
def notify(message):
"""Test notifier, stores notifications in memory for unittests."""
NOTIFICATIONS.append(message)

View File

@ -275,6 +275,11 @@ class FanoutAdapterConsumer(AdapterConsumer):
unique = uuid.uuid4().hex unique = uuid.uuid4().hex
self.queue = '%s_fanout_%s' % (topic, unique) self.queue = '%s_fanout_%s' % (topic, unique)
self.durable = False self.durable = False
# Fanout creates unique queue names, so we should auto-remove
# them when done, so they're not left around on restart.
# Also, we're the only one that should be consuming. exclusive
# implies auto_delete, so we'll just set that..
self.exclusive = True
LOG.info(_('Created "%(exchange)s" fanout exchange ' LOG.info(_('Created "%(exchange)s" fanout exchange '
'with "%(key)s" routing key'), 'with "%(key)s" routing key'),
dict(exchange=self.exchange, key=self.routing_key)) dict(exchange=self.exchange, key=self.routing_key))

View File

@ -147,6 +147,16 @@ def stub_out_compute_api_snapshot(stubs):
stubs.Set(nova.compute.API, 'snapshot', snapshot) stubs.Set(nova.compute.API, 'snapshot', snapshot)
def stub_out_compute_api_backup(stubs):
def backup(self, context, instance_id, name, backup_type, rotation,
extra_properties=None):
props = dict(instance_id=instance_id, instance_ref=instance_id,
backup_type=backup_type, rotation=rotation)
props.update(extra_properties or {})
return dict(id='123', status='ACTIVE', name=name, properties=props)
stubs.Set(nova.compute.API, 'backup', backup)
def stub_out_glance_add_image(stubs, sent_to_glance): def stub_out_glance_add_image(stubs, sent_to_glance):
""" """
We return the metadata sent to glance by modifying the sent_to_glance dict We return the metadata sent to glance by modifying the sent_to_glance dict

View File

@ -161,12 +161,12 @@ class PaginationParamsTest(test.TestCase):
def test_no_params(self): def test_no_params(self):
""" Test no params. """ """ Test no params. """
req = Request.blank('/') req = Request.blank('/')
self.assertEqual(common.get_pagination_params(req), (0, 0)) self.assertEqual(common.get_pagination_params(req), {})
def test_valid_marker(self): def test_valid_marker(self):
""" Test valid marker param. """ """ Test valid marker param. """
req = Request.blank('/?marker=1') req = Request.blank('/?marker=1')
self.assertEqual(common.get_pagination_params(req), (1, 0)) self.assertEqual(common.get_pagination_params(req), {'marker': 1})
def test_invalid_marker(self): def test_invalid_marker(self):
""" Test invalid marker param. """ """ Test invalid marker param. """
@ -177,10 +177,16 @@ class PaginationParamsTest(test.TestCase):
def test_valid_limit(self): def test_valid_limit(self):
""" Test valid limit param. """ """ Test valid limit param. """
req = Request.blank('/?limit=10') req = Request.blank('/?limit=10')
self.assertEqual(common.get_pagination_params(req), (0, 10)) self.assertEqual(common.get_pagination_params(req), {'limit': 10})
def test_invalid_limit(self): def test_invalid_limit(self):
""" Test invalid limit param. """ """ Test invalid limit param. """
req = Request.blank('/?limit=-2') req = Request.blank('/?limit=-2')
self.assertRaises( self.assertRaises(
webob.exc.HTTPBadRequest, common.get_pagination_params, req) webob.exc.HTTPBadRequest, common.get_pagination_params, req)
def test_valid_limit_and_marker(self):
""" Test valid limit and marker parameters. """
req = Request.blank('/?limit=20&marker=40')
self.assertEqual(common.get_pagination_params(req),
{'marker': 40, 'limit': 20})

View File

@ -340,6 +340,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
self.fixtures = self._make_image_fixtures() self.fixtures = self._make_image_fixtures()
fakes.stub_out_glance(self.stubs, initial_fixtures=self.fixtures) fakes.stub_out_glance(self.stubs, initial_fixtures=self.fixtures)
fakes.stub_out_compute_api_snapshot(self.stubs) fakes.stub_out_compute_api_snapshot(self.stubs)
fakes.stub_out_compute_api_backup(self.stubs)
def tearDown(self): def tearDown(self):
"""Run after each test.""" """Run after each test."""
@ -364,10 +365,10 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
response_list = response_dict["images"] response_list = response_dict["images"]
expected = [{'id': 123, 'name': 'public image'}, expected = [{'id': 123, 'name': 'public image'},
{'id': 124, 'name': 'queued backup'}, {'id': 124, 'name': 'queued snapshot'},
{'id': 125, 'name': 'saving backup'}, {'id': 125, 'name': 'saving snapshot'},
{'id': 126, 'name': 'active backup'}, {'id': 126, 'name': 'active snapshot'},
{'id': 127, 'name': 'killed backup'}, {'id': 127, 'name': 'killed snapshot'},
{'id': 129, 'name': None}] {'id': 129, 'name': None}]
self.assertDictListMatch(response_list, expected) self.assertDictListMatch(response_list, expected)
@ -617,14 +618,14 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
}, },
{ {
'id': 124, 'id': 124,
'name': 'queued backup', 'name': 'queued snapshot',
'updated': self.NOW_API_FORMAT, 'updated': self.NOW_API_FORMAT,
'created': self.NOW_API_FORMAT, 'created': self.NOW_API_FORMAT,
'status': 'QUEUED', 'status': 'QUEUED',
}, },
{ {
'id': 125, 'id': 125,
'name': 'saving backup', 'name': 'saving snapshot',
'updated': self.NOW_API_FORMAT, 'updated': self.NOW_API_FORMAT,
'created': self.NOW_API_FORMAT, 'created': self.NOW_API_FORMAT,
'status': 'SAVING', 'status': 'SAVING',
@ -632,14 +633,14 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
}, },
{ {
'id': 126, 'id': 126,
'name': 'active backup', 'name': 'active snapshot',
'updated': self.NOW_API_FORMAT, 'updated': self.NOW_API_FORMAT,
'created': self.NOW_API_FORMAT, 'created': self.NOW_API_FORMAT,
'status': 'ACTIVE' 'status': 'ACTIVE'
}, },
{ {
'id': 127, 'id': 127,
'name': 'killed backup', 'name': 'killed snapshot',
'updated': self.NOW_API_FORMAT, 'updated': self.NOW_API_FORMAT,
'created': self.NOW_API_FORMAT, 'created': self.NOW_API_FORMAT,
'status': 'FAILED', 'status': 'FAILED',
@ -684,7 +685,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
}, },
{ {
'id': 124, 'id': 124,
'name': 'queued backup', 'name': 'queued snapshot',
'serverRef': "http://localhost:8774/v1.1/servers/42", 'serverRef': "http://localhost:8774/v1.1/servers/42",
'updated': self.NOW_API_FORMAT, 'updated': self.NOW_API_FORMAT,
'created': self.NOW_API_FORMAT, 'created': self.NOW_API_FORMAT,
@ -706,7 +707,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
}, },
{ {
'id': 125, 'id': 125,
'name': 'saving backup', 'name': 'saving snapshot',
'serverRef': "http://localhost:8774/v1.1/servers/42", 'serverRef': "http://localhost:8774/v1.1/servers/42",
'updated': self.NOW_API_FORMAT, 'updated': self.NOW_API_FORMAT,
'created': self.NOW_API_FORMAT, 'created': self.NOW_API_FORMAT,
@ -729,7 +730,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
}, },
{ {
'id': 126, 'id': 126,
'name': 'active backup', 'name': 'active snapshot',
'serverRef': "http://localhost:8774/v1.1/servers/42", 'serverRef': "http://localhost:8774/v1.1/servers/42",
'updated': self.NOW_API_FORMAT, 'updated': self.NOW_API_FORMAT,
'created': self.NOW_API_FORMAT, 'created': self.NOW_API_FORMAT,
@ -751,7 +752,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
}, },
{ {
'id': 127, 'id': 127,
'name': 'killed backup', 'name': 'killed snapshot',
'serverRef': "http://localhost:8774/v1.1/servers/42", 'serverRef': "http://localhost:8774/v1.1/servers/42",
'updated': self.NOW_API_FORMAT, 'updated': self.NOW_API_FORMAT,
'created': self.NOW_API_FORMAT, 'created': self.NOW_API_FORMAT,
@ -802,7 +803,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
context = object() context = object()
filters = {'name': 'testname'} filters = {'name': 'testname'}
image_service.index( image_service.index(
context, filters=filters, marker=0, limit=0).AndReturn([]) context, filters=filters).AndReturn([])
mocker.ReplayAll() mocker.ReplayAll()
request = webob.Request.blank( request = webob.Request.blank(
'/v1.1/images?name=testname') '/v1.1/images?name=testname')
@ -817,7 +818,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
context = object() context = object()
filters = {'status': 'ACTIVE'} filters = {'status': 'ACTIVE'}
image_service.index( image_service.index(
context, filters=filters, marker=0, limit=0).AndReturn([]) context, filters=filters).AndReturn([])
mocker.ReplayAll() mocker.ReplayAll()
request = webob.Request.blank( request = webob.Request.blank(
'/v1.1/images?status=ACTIVE') '/v1.1/images?status=ACTIVE')
@ -832,7 +833,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
context = object() context = object()
filters = {'property-test': '3'} filters = {'property-test': '3'}
image_service.index( image_service.index(
context, filters=filters, marker=0, limit=0).AndReturn([]) context, filters=filters).AndReturn([])
mocker.ReplayAll() mocker.ReplayAll()
request = webob.Request.blank( request = webob.Request.blank(
'/v1.1/images?property-test=3') '/v1.1/images?property-test=3')
@ -847,7 +848,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
context = object() context = object()
filters = {'status': 'ACTIVE'} filters = {'status': 'ACTIVE'}
image_service.index( image_service.index(
context, filters=filters, marker=0, limit=0).AndReturn([]) context, filters=filters).AndReturn([])
mocker.ReplayAll() mocker.ReplayAll()
request = webob.Request.blank( request = webob.Request.blank(
'/v1.1/images?status=ACTIVE&UNSUPPORTEDFILTER=testname') '/v1.1/images?status=ACTIVE&UNSUPPORTEDFILTER=testname')
@ -862,7 +863,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
context = object() context = object()
filters = {} filters = {}
image_service.index( image_service.index(
context, filters=filters, marker=0, limit=0).AndReturn([]) context, filters=filters).AndReturn([])
mocker.ReplayAll() mocker.ReplayAll()
request = webob.Request.blank( request = webob.Request.blank(
'/v1.1/images') '/v1.1/images')
@ -877,7 +878,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
context = object() context = object()
filters = {'name': 'testname'} filters = {'name': 'testname'}
image_service.detail( image_service.detail(
context, filters=filters, marker=0, limit=0).AndReturn([]) context, filters=filters).AndReturn([])
mocker.ReplayAll() mocker.ReplayAll()
request = webob.Request.blank( request = webob.Request.blank(
'/v1.1/images/detail?name=testname') '/v1.1/images/detail?name=testname')
@ -892,7 +893,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
context = object() context = object()
filters = {'status': 'ACTIVE'} filters = {'status': 'ACTIVE'}
image_service.detail( image_service.detail(
context, filters=filters, marker=0, limit=0).AndReturn([]) context, filters=filters).AndReturn([])
mocker.ReplayAll() mocker.ReplayAll()
request = webob.Request.blank( request = webob.Request.blank(
'/v1.1/images/detail?status=ACTIVE') '/v1.1/images/detail?status=ACTIVE')
@ -907,7 +908,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
context = object() context = object()
filters = {'property-test': '3'} filters = {'property-test': '3'}
image_service.detail( image_service.detail(
context, filters=filters, marker=0, limit=0).AndReturn([]) context, filters=filters).AndReturn([])
mocker.ReplayAll() mocker.ReplayAll()
request = webob.Request.blank( request = webob.Request.blank(
'/v1.1/images/detail?property-test=3') '/v1.1/images/detail?property-test=3')
@ -922,7 +923,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
context = object() context = object()
filters = {'status': 'ACTIVE'} filters = {'status': 'ACTIVE'}
image_service.detail( image_service.detail(
context, filters=filters, marker=0, limit=0).AndReturn([]) context, filters=filters).AndReturn([])
mocker.ReplayAll() mocker.ReplayAll()
request = webob.Request.blank( request = webob.Request.blank(
'/v1.1/images/detail?status=ACTIVE&UNSUPPORTEDFILTER=testname') '/v1.1/images/detail?status=ACTIVE&UNSUPPORTEDFILTER=testname')
@ -937,7 +938,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
context = object() context = object()
filters = {} filters = {}
image_service.detail( image_service.detail(
context, filters=filters, marker=0, limit=0).AndReturn([]) context, filters=filters).AndReturn([])
mocker.ReplayAll() mocker.ReplayAll()
request = webob.Request.blank( request = webob.Request.blank(
'/v1.1/images/detail') '/v1.1/images/detail')
@ -969,8 +970,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
self.assertEqual(res.status_int, 404) self.assertEqual(res.status_int, 404)
def test_create_image(self): def test_create_image(self):
body = dict(image=dict(serverId='123', name='Snapshot 1'))
body = dict(image=dict(serverId='123', name='Backup 1'))
req = webob.Request.blank('/v1.0/images') req = webob.Request.blank('/v1.0/images')
req.method = 'POST' req.method = 'POST'
req.body = json.dumps(body) req.body = json.dumps(body)
@ -978,9 +978,95 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
response = req.get_response(fakes.wsgi_app()) response = req.get_response(fakes.wsgi_app())
self.assertEqual(200, response.status_int) self.assertEqual(200, response.status_int)
def test_create_snapshot_no_name(self):
"""Name is required for snapshots"""
body = dict(image=dict(serverId='123'))
req = webob.Request.blank('/v1.0/images')
req.method = 'POST'
req.body = json.dumps(body)
req.headers["content-type"] = "application/json"
response = req.get_response(fakes.wsgi_app())
self.assertEqual(400, response.status_int)
def test_create_backup_no_name(self):
"""Name is also required for backups"""
body = dict(image=dict(serverId='123', image_type='backup',
backup_type='daily', rotation=1))
req = webob.Request.blank('/v1.0/images')
req.method = 'POST'
req.body = json.dumps(body)
req.headers["content-type"] = "application/json"
response = req.get_response(fakes.wsgi_app())
self.assertEqual(400, response.status_int)
def test_create_backup_with_rotation_and_backup_type(self):
"""The happy path for creating backups
Creating a backup is an admin-only operation, as opposed to snapshots
which are available to anybody.
"""
# FIXME(sirp): teardown needed?
FLAGS.allow_admin_api = True
# FIXME(sirp): should the fact that backups are admin_only be a FLAG
body = dict(image=dict(serverId='123', image_type='backup',
name='Backup 1',
backup_type='daily', rotation=1))
req = webob.Request.blank('/v1.0/images')
req.method = 'POST'
req.body = json.dumps(body)
req.headers["content-type"] = "application/json"
response = req.get_response(fakes.wsgi_app())
self.assertEqual(200, response.status_int)
def test_create_backup_no_rotation(self):
"""Rotation is required for backup requests"""
# FIXME(sirp): teardown needed?
FLAGS.allow_admin_api = True
# FIXME(sirp): should the fact that backups are admin_only be a FLAG
body = dict(image=dict(serverId='123', name='daily',
image_type='backup', backup_type='daily'))
req = webob.Request.blank('/v1.0/images')
req.method = 'POST'
req.body = json.dumps(body)
req.headers["content-type"] = "application/json"
response = req.get_response(fakes.wsgi_app())
self.assertEqual(400, response.status_int)
def test_create_backup_no_backup_type(self):
"""Backup Type (daily or weekly) is required for backup requests"""
# FIXME(sirp): teardown needed?
FLAGS.allow_admin_api = True
# FIXME(sirp): should the fact that backups are admin_only be a FLAG
body = dict(image=dict(serverId='123', name='daily',
image_type='backup', rotation=1))
req = webob.Request.blank('/v1.0/images')
req.method = 'POST'
req.body = json.dumps(body)
req.headers["content-type"] = "application/json"
response = req.get_response(fakes.wsgi_app())
self.assertEqual(400, response.status_int)
def test_create_image_with_invalid_image_type(self):
"""Valid image_types are snapshot | daily | weekly"""
# FIXME(sirp): teardown needed?
FLAGS.allow_admin_api = True
# FIXME(sirp): should the fact that backups are admin_only be a FLAG
body = dict(image=dict(serverId='123', image_type='monthly',
rotation=1))
req = webob.Request.blank('/v1.0/images')
req.method = 'POST'
req.body = json.dumps(body)
req.headers["content-type"] = "application/json"
response = req.get_response(fakes.wsgi_app())
self.assertEqual(400, response.status_int)
def test_create_image_no_server_id(self): def test_create_image_no_server_id(self):
body = dict(image=dict(name='Backup 1')) body = dict(image=dict(name='Snapshot 1'))
req = webob.Request.blank('/v1.0/images') req = webob.Request.blank('/v1.0/images')
req.method = 'POST' req.method = 'POST'
req.body = json.dumps(body) req.body = json.dumps(body)
@ -990,7 +1076,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
def test_create_image_v1_1(self): def test_create_image_v1_1(self):
body = dict(image=dict(serverRef='123', name='Backup 1')) body = dict(image=dict(serverRef='123', name='Snapshot 1'))
req = webob.Request.blank('/v1.1/images') req = webob.Request.blank('/v1.1/images')
req.method = 'POST' req.method = 'POST'
req.body = json.dumps(body) req.body = json.dumps(body)
@ -1024,7 +1110,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
def test_create_image_v1_1_xml_serialization(self): def test_create_image_v1_1_xml_serialization(self):
body = dict(image=dict(serverRef='123', name='Backup 1')) body = dict(image=dict(serverRef='123', name='Snapshot 1'))
req = webob.Request.blank('/v1.1/images') req = webob.Request.blank('/v1.1/images')
req.method = 'POST' req.method = 'POST'
req.body = json.dumps(body) req.body = json.dumps(body)
@ -1038,7 +1124,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
<image <image
created="None" created="None"
id="123" id="123"
name="Backup 1" name="Snapshot 1"
serverRef="http://localhost/v1.1/servers/123" serverRef="http://localhost/v1.1/servers/123"
status="ACTIVE" status="ACTIVE"
updated="None" updated="None"
@ -1057,7 +1143,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
def test_create_image_v1_1_no_server_ref(self): def test_create_image_v1_1_no_server_ref(self):
body = dict(image=dict(name='Backup 1')) body = dict(image=dict(name='Snapshot 1'))
req = webob.Request.blank('/v1.1/images') req = webob.Request.blank('/v1.1/images')
req.method = 'POST' req.method = 'POST'
req.body = json.dumps(body) req.body = json.dumps(body)
@ -1084,19 +1170,21 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
status='active', properties={}) status='active', properties={})
image_id += 1 image_id += 1
# Backup for User 1 # Snapshot for User 1
server_ref = 'http://localhost:8774/v1.1/servers/42' server_ref = 'http://localhost:8774/v1.1/servers/42'
backup_properties = {'instance_ref': server_ref, 'user_id': '1'} snapshot_properties = {'instance_ref': server_ref, 'user_id': '1'}
for status in ('queued', 'saving', 'active', 'killed'): for status in ('queued', 'saving', 'active', 'killed'):
add_fixture(id=image_id, name='%s backup' % status, add_fixture(id=image_id, name='%s snapshot' % status,
is_public=False, status=status, is_public=False, status=status,
properties=backup_properties) properties=snapshot_properties)
image_id += 1 image_id += 1
# Backup for User 2 # Snapshot for User 2
other_backup_properties = {'instance_id': '43', 'user_id': '2'} other_snapshot_properties = {'instance_id': '43', 'user_id': '2'}
add_fixture(id=image_id, name='someone elses backup', is_public=False, add_fixture(id=image_id, name='someone elses snapshot',
status='active', properties=other_backup_properties) is_public=False, status='active',
properties=other_snapshot_properties)
image_id += 1 image_id += 1
# Image without a name # Image without a name

View File

@ -37,6 +37,7 @@ from nova import log as logging
from nova import rpc from nova import rpc
from nova import test from nova import test
from nova import utils from nova import utils
from nova.notifier import test_notifier
LOG = logging.getLogger('nova.tests.compute') LOG = logging.getLogger('nova.tests.compute')
FLAGS = flags.FLAGS FLAGS = flags.FLAGS
@ -62,6 +63,7 @@ class ComputeTestCase(test.TestCase):
super(ComputeTestCase, self).setUp() super(ComputeTestCase, self).setUp()
self.flags(connection_type='fake', self.flags(connection_type='fake',
stub_network=True, stub_network=True,
notification_driver='nova.notifier.test_notifier',
network_manager='nova.network.manager.FlatManager') network_manager='nova.network.manager.FlatManager')
self.compute = utils.import_object(FLAGS.compute_manager) self.compute = utils.import_object(FLAGS.compute_manager)
self.compute_api = compute.API() self.compute_api = compute.API()
@ -69,6 +71,7 @@ class ComputeTestCase(test.TestCase):
self.user = self.manager.create_user('fake', 'fake', 'fake') self.user = self.manager.create_user('fake', 'fake', 'fake')
self.project = self.manager.create_project('fake', 'fake', 'fake') self.project = self.manager.create_project('fake', 'fake', 'fake')
self.context = context.RequestContext('fake', 'fake', False) self.context = context.RequestContext('fake', 'fake', False)
test_notifier.NOTIFICATIONS = []
def fake_show(meh, context, id): def fake_show(meh, context, id):
return {'id': 1, 'properties': {'kernel_id': 1, 'ramdisk_id': 1}} return {'id': 1, 'properties': {'kernel_id': 1, 'ramdisk_id': 1}}
@ -326,6 +329,50 @@ class ComputeTestCase(test.TestCase):
self.assert_(console) self.assert_(console)
self.compute.terminate_instance(self.context, instance_id) self.compute.terminate_instance(self.context, instance_id)
def test_run_instance_usage_notification(self):
"""Ensure run instance generates apropriate usage notification"""
instance_id = self._create_instance()
self.compute.run_instance(self.context, instance_id)
self.assertEquals(len(test_notifier.NOTIFICATIONS), 1)
msg = test_notifier.NOTIFICATIONS[0]
self.assertEquals(msg['priority'], 'INFO')
self.assertEquals(msg['event_type'], 'compute.instance.create')
payload = msg['payload']
self.assertEquals(payload['tenant_id'], self.project.id)
self.assertEquals(payload['user_id'], self.user.id)
self.assertEquals(payload['instance_id'], instance_id)
self.assertEquals(payload['instance_type'], 'm1.tiny')
type_id = instance_types.get_instance_type_by_name('m1.tiny')['id']
self.assertEquals(str(payload['instance_type_id']), str(type_id))
self.assertTrue('display_name' in payload)
self.assertTrue('created_at' in payload)
self.assertTrue('launched_at' in payload)
self.assertEquals(payload['image_ref'], '1')
self.compute.terminate_instance(self.context, instance_id)
def test_terminate_usage_notification(self):
"""Ensure terminate_instance generates apropriate usage notification"""
instance_id = self._create_instance()
self.compute.run_instance(self.context, instance_id)
test_notifier.NOTIFICATIONS = []
self.compute.terminate_instance(self.context, instance_id)
self.assertEquals(len(test_notifier.NOTIFICATIONS), 1)
msg = test_notifier.NOTIFICATIONS[0]
self.assertEquals(msg['priority'], 'INFO')
self.assertEquals(msg['event_type'], 'compute.instance.delete')
payload = msg['payload']
self.assertEquals(payload['tenant_id'], self.project.id)
self.assertEquals(payload['user_id'], self.user.id)
self.assertEquals(payload['instance_id'], instance_id)
self.assertEquals(payload['instance_type'], 'm1.tiny')
type_id = instance_types.get_instance_type_by_name('m1.tiny')['id']
self.assertEquals(str(payload['instance_type_id']), str(type_id))
self.assertTrue('display_name' in payload)
self.assertTrue('created_at' in payload)
self.assertTrue('launched_at' in payload)
self.assertEquals(payload['image_ref'], '1')
def test_run_instance_existing(self): def test_run_instance_existing(self):
"""Ensure failure when running an instance that already exists""" """Ensure failure when running an instance that already exists"""
instance_id = self._create_instance() instance_id = self._create_instance()
@ -378,6 +425,36 @@ class ComputeTestCase(test.TestCase):
self.compute.terminate_instance(self.context, instance_id) self.compute.terminate_instance(self.context, instance_id)
def test_resize_instance_notification(self):
"""Ensure notifications on instance migrate/resize"""
instance_id = self._create_instance()
context = self.context.elevated()
self.compute.run_instance(self.context, instance_id)
test_notifier.NOTIFICATIONS = []
db.instance_update(self.context, instance_id, {'host': 'foo'})
self.compute.prep_resize(context, instance_id, 1)
migration_ref = db.migration_get_by_instance_and_status(context,
instance_id, 'pre-migrating')
self.assertEquals(len(test_notifier.NOTIFICATIONS), 1)
msg = test_notifier.NOTIFICATIONS[0]
self.assertEquals(msg['priority'], 'INFO')
self.assertEquals(msg['event_type'], 'compute.instance.resize.prep')
payload = msg['payload']
self.assertEquals(payload['tenant_id'], self.project.id)
self.assertEquals(payload['user_id'], self.user.id)
self.assertEquals(payload['instance_id'], instance_id)
self.assertEquals(payload['instance_type'], 'm1.tiny')
type_id = instance_types.get_instance_type_by_name('m1.tiny')['id']
self.assertEquals(str(payload['instance_type_id']), str(type_id))
self.assertTrue('display_name' in payload)
self.assertTrue('created_at' in payload)
self.assertTrue('launched_at' in payload)
self.assertEquals(payload['image_ref'], '1')
self.compute.terminate_instance(context, instance_id)
def test_resize_instance(self): def test_resize_instance(self):
"""Ensure instance can be migrated/resized""" """Ensure instance can be migrated/resized"""
instance_id = self._create_instance() instance_id = self._create_instance()

View File

@ -274,6 +274,22 @@ EASIER_PASSWORD_SYMBOLS = ('23456789' # Removed: 0, 1
'ABCDEFGHJKLMNPQRSTUVWXYZ') # Removed: I, O 'ABCDEFGHJKLMNPQRSTUVWXYZ') # Removed: I, O
def usage_from_instance(instance_ref, **kw):
usage_info = dict(
tenant_id=instance_ref['project_id'],
user_id=instance_ref['user_id'],
instance_id=instance_ref['id'],
instance_type=instance_ref['instance_type']['name'],
instance_type_id=instance_ref['instance_type_id'],
display_name=instance_ref['display_name'],
created_at=str(instance_ref['created_at']),
launched_at=str(instance_ref['launched_at']) \
if instance_ref['launched_at'] else '',
image_ref=instance_ref['image_ref'])
usage_info.update(kw)
return usage_info
def generate_password(length=20, symbols=DEFAULT_PASSWORD_SYMBOLS): def generate_password(length=20, symbols=DEFAULT_PASSWORD_SYMBOLS):
"""Generate a random password from the supplied symbols. """Generate a random password from the supplied symbols.

View File

@ -0,0 +1,20 @@
#!/bin/bash
PACKAGE=openstack-xen-plugins
RPMBUILD_DIR=$PWD/rpmbuild
if [ ! -d $RPMBUILD_DIR ]; then
echo $RPMBUILD_DIR is missing
exit 1
fi
for dir in BUILD BUILDROOT SRPMS RPMS SOURCES; do
rm -rf $RPMBUILD_DIR/$dir
mkdir -p $RPMBUILD_DIR/$dir
done
rm -rf /tmp/$PACKAGE
mkdir /tmp/$PACKAGE
cp -r ../etc/xapi.d /tmp/$PACKAGE
tar czf $RPMBUILD_DIR/SOURCES/$PACKAGE.tar.gz -C /tmp $PACKAGE
rpmbuild -ba --nodeps --define "_topdir $RPMBUILD_DIR" \
$RPMBUILD_DIR/SPECS/$PACKAGE.spec

View File

@ -0,0 +1,36 @@
Name: openstack-xen-plugins
Version: 2011.3
Release: 1
Summary: Files for XenAPI support.
License: ASL 2.0
Group: Applications/Utilities
Source0: openstack-xen-plugins.tar.gz
BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-root-%(%{__id_u} -n)
Requires: parted
%define debug_package %{nil}
%description
This package contains files that are required for XenAPI support for OpenStack.
%prep
%setup -q -n openstack-xen-plugins
%install
rm -rf $RPM_BUILD_ROOT
mkdir -p $RPM_BUILD_ROOT/etc
cp -r xapi.d $RPM_BUILD_ROOT/etc
chmod a+x $RPM_BUILD_ROOT/etc/xapi.d/plugins/*
%clean
rm -rf $RPM_BUILD_ROOT
%files
%defattr(-,root,root,-)
/etc/xapi.d/plugins/agent
/etc/xapi.d/plugins/glance
/etc/xapi.d/plugins/migration
/etc/xapi.d/plugins/objectstore
/etc/xapi.d/plugins/pluginlib_nova.py
/etc/xapi.d/plugins/xenhost
/etc/xapi.d/plugins/xenstore.py

View File

@ -6,7 +6,8 @@ function usage {
echo "" echo ""
echo " -V, --virtual-env Always use virtualenv. Install automatically if not present" echo " -V, --virtual-env Always use virtualenv. Install automatically if not present"
echo " -N, --no-virtual-env Don't use virtualenv. Run tests in local environment" echo " -N, --no-virtual-env Don't use virtualenv. Run tests in local environment"
echo " -r, --recreate-db Recreate the test database." echo " -r, --recreate-db Recreate the test database (deprecated, as this is now the default)."
echo " -n, --no-recreate-db Don't recreate the test database."
echo " -x, --stop Stop running tests after the first error or failure." echo " -x, --stop Stop running tests after the first error or failure."
echo " -f, --force Force a clean re-build of the virtual environment. Useful when dependencies have been added." echo " -f, --force Force a clean re-build of the virtual environment. Useful when dependencies have been added."
echo " -p, --pep8 Just run pep8" echo " -p, --pep8 Just run pep8"
@ -25,6 +26,7 @@ function process_option {
-V|--virtual-env) let always_venv=1; let never_venv=0;; -V|--virtual-env) let always_venv=1; let never_venv=0;;
-N|--no-virtual-env) let always_venv=0; let never_venv=1;; -N|--no-virtual-env) let always_venv=0; let never_venv=1;;
-r|--recreate-db) let recreate_db=1;; -r|--recreate-db) let recreate_db=1;;
-n|--no-recreate-db) let recreate_db=0;;
-f|--force) let force=1;; -f|--force) let force=1;;
-p|--pep8) let just_pep8=1;; -p|--pep8) let just_pep8=1;;
-*) noseopts="$noseopts $1";; -*) noseopts="$noseopts $1";;
@ -41,7 +43,7 @@ noseargs=
noseopts= noseopts=
wrapper="" wrapper=""
just_pep8=0 just_pep8=0
recreate_db=0 recreate_db=1
for arg in "$@"; do for arg in "$@"; do
process_option $arg process_option $arg