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
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.utils

View File

@ -644,7 +644,7 @@ class VmCommands(object):
:param host: show all instance on specified host.
: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" % (
_('instance'),
_('node'),
@ -666,14 +666,14 @@ class VmCommands(object):
context.get_admin_context(), host)
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" % (
instance['hostname'],
instance['host'],
instance['instance_type'],
instance['instance_type'].name,
instance['state_description'],
instance['launched_at'],
instance['image_id'],
instance['image_ref'],
instance['kernel_id'],
instance['ramdisk_id'],
instance['project_id'],
@ -905,7 +905,7 @@ class InstanceTypeCommands(object):
try:
instance_types.create(name, memory, vcpus, local_gb,
flavorid, swap, rxtx_quota, rxtx_cap)
except exception.InvalidInput:
except exception.InvalidInput, e:
print "Must supply valid parameters to create instance_type"
print e
sys.exit(1)

View File

@ -45,23 +45,20 @@ def get_pagination_params(request):
exc.HTTPBadRequest() exceptions to be raised.
"""
try:
marker = int(request.GET.get('marker', 0))
except ValueError:
raise webob.exc.HTTPBadRequest(_('marker param must be an integer'))
params = {}
for param in ['marker', 'limit']:
if not param in request.GET:
continue
try:
params[param] = int(request.GET[param])
except ValueError:
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:
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)
return params
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):
"""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 = max_limit
limit = params.get('limit', max_limit)
marker = params.get('marker')
limit = min(max_limit, limit)
start_index = 0

View File

@ -90,31 +90,67 @@ class Controller(object):
return webob.exc.HTTPNoContent()
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
"""
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']
content_type = req.get_content_type()
if not body:
raise webob.exc.HTTPBadRequest()
image_type = body["image"].get("image_type", "snapshot")
try:
server_id = self._server_id_from_req(req, body)
image_name = body["image"]["name"]
except KeyError:
raise webob.exc.HTTPBadRequest()
image_name = get_param("name")
props = self._get_extra_properties(req, body)
image = self._compute_service.snapshot(context, server_id,
image_name, props)
if image_type == "snapshot":
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))
def get_builder(self, request):
"""Indicates that you must use a Controller subclass."""
raise NotImplementedError
raise NotImplementedError()
def _server_id_from_req(self, req, data):
raise NotImplementedError()
@ -181,9 +217,9 @@ class ControllerV11(Controller):
"""
context = req.environ['nova.context']
filters = self._get_filters(req)
(marker, limit) = common.get_pagination_params(req)
images = self._image_service.index(
context, filters=filters, marker=marker, limit=limit)
page_params = common.get_pagination_params(req)
images = self._image_service.index(context, filters=filters,
**page_params)
builder = self.get_builder(req).build
return dict(images=[builder(image, detail=False) for image in images])
@ -195,9 +231,9 @@ class ControllerV11(Controller):
"""
context = req.environ['nova.context']
filters = self._get_filters(req)
(marker, limit) = common.get_pagination_params(req)
images = self._image_service.detail(
context, filters=filters, marker=marker, limit=limit)
page_params = common.get_pagination_params(req)
images = self._image_service.detail(context, filters=filters,
**page_params)
builder = self.get_builder(req).build
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")
% 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):
"""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
"""
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),
'image_state': 'creating'}
'image_state': 'creating',
'image_type': image_type,
'backup_type': backup_type}
properties.update(extra_properties or {})
sent_meta = {'name': name, 'is_public': False,
'status': 'creating', 'properties': properties}
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,
params=params)
return recv_meta

View File

@ -46,13 +46,16 @@ from eventlet import greenthread
from nova import exception
from nova import flags
import nova.image
from nova import log as logging
from nova import manager
from nova import network
from nova import notifier
from nova import rpc
from nova import utils
from nova import volume
from nova.compute import power_state
from nova.notifier import api as notifier_api
from nova.compute.utils import terminate_volumes
from nova.virt import driver
@ -315,6 +318,11 @@ class ComputeManager(manager.SchedulerDependentManager):
self._update_launched_at(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:
# FIXME(wwolf): We are just ignoring InstanceNotFound
# exceptions here in case the instance was immediately
@ -364,9 +372,15 @@ class ComputeManager(manager.SchedulerDependentManager):
def terminate_instance(self, context, instance_id):
"""Terminate an instance on this host."""
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?
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
@checks_instance_lock
@ -403,6 +417,12 @@ class ComputeManager(manager.SchedulerDependentManager):
self._update_image_ref(context, instance_id, image_ref)
self._update_launched_at(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
@checks_instance_lock
@ -430,8 +450,19 @@ class ComputeManager(manager.SchedulerDependentManager):
self._update_state(context, instance_id)
@exception.wrap_exception
def snapshot_instance(self, context, instance_id, image_id):
"""Snapshot an instance on this host."""
def snapshot_instance(self, context, instance_id, image_id,
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()
instance_ref = self.db.instance_get(context, instance_id)
@ -451,6 +482,65 @@ class ComputeManager(manager.SchedulerDependentManager):
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
@checks_instance_lock
def set_admin_password(self, context, instance_id, new_pass=None):
@ -580,6 +670,11 @@ class ComputeManager(manager.SchedulerDependentManager):
context = context.elevated()
instance_ref = self.db.instance_get(context, instance_id)
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
@checks_instance_lock
@ -627,6 +722,11 @@ class ComputeManager(manager.SchedulerDependentManager):
self.driver.revert_resize(instance_ref)
self.db.migration_update(context, migration_id,
{'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
@checks_instance_lock
@ -663,6 +763,13 @@ class ComputeManager(manager.SchedulerDependentManager):
'migration_id': migration_ref['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
@checks_instance_lock

View File

@ -494,6 +494,11 @@ def 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):
"""Get all instances."""
return IMPL.instance_get_all_by_user(context, user_id)

View File

@ -1136,6 +1136,24 @@ def instance_get_all(context):
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
def instance_get_all_by_user(context, user_id):
session = get_session()

View File

@ -591,6 +591,14 @@ class GlobalRoleNotAllowed(NotAllowed):
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!
class Duplicate(NovaException):
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
self.queue = '%s_fanout_%s' % (topic, unique)
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 '
'with "%(key)s" 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)
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):
"""
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):
""" Test no params. """
req = Request.blank('/')
self.assertEqual(common.get_pagination_params(req), (0, 0))
self.assertEqual(common.get_pagination_params(req), {})
def test_valid_marker(self):
""" Test valid marker param. """
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):
""" Test invalid marker param. """
@ -177,10 +177,16 @@ class PaginationParamsTest(test.TestCase):
def test_valid_limit(self):
""" Test valid limit param. """
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):
""" Test invalid limit param. """
req = Request.blank('/?limit=-2')
self.assertRaises(
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()
fakes.stub_out_glance(self.stubs, initial_fixtures=self.fixtures)
fakes.stub_out_compute_api_snapshot(self.stubs)
fakes.stub_out_compute_api_backup(self.stubs)
def tearDown(self):
"""Run after each test."""
@ -364,10 +365,10 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
response_list = response_dict["images"]
expected = [{'id': 123, 'name': 'public image'},
{'id': 124, 'name': 'queued backup'},
{'id': 125, 'name': 'saving backup'},
{'id': 126, 'name': 'active backup'},
{'id': 127, 'name': 'killed backup'},
{'id': 124, 'name': 'queued snapshot'},
{'id': 125, 'name': 'saving snapshot'},
{'id': 126, 'name': 'active snapshot'},
{'id': 127, 'name': 'killed snapshot'},
{'id': 129, 'name': None}]
self.assertDictListMatch(response_list, expected)
@ -617,14 +618,14 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
},
{
'id': 124,
'name': 'queued backup',
'name': 'queued snapshot',
'updated': self.NOW_API_FORMAT,
'created': self.NOW_API_FORMAT,
'status': 'QUEUED',
},
{
'id': 125,
'name': 'saving backup',
'name': 'saving snapshot',
'updated': self.NOW_API_FORMAT,
'created': self.NOW_API_FORMAT,
'status': 'SAVING',
@ -632,14 +633,14 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
},
{
'id': 126,
'name': 'active backup',
'name': 'active snapshot',
'updated': self.NOW_API_FORMAT,
'created': self.NOW_API_FORMAT,
'status': 'ACTIVE'
},
{
'id': 127,
'name': 'killed backup',
'name': 'killed snapshot',
'updated': self.NOW_API_FORMAT,
'created': self.NOW_API_FORMAT,
'status': 'FAILED',
@ -684,7 +685,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
},
{
'id': 124,
'name': 'queued backup',
'name': 'queued snapshot',
'serverRef': "http://localhost:8774/v1.1/servers/42",
'updated': self.NOW_API_FORMAT,
'created': self.NOW_API_FORMAT,
@ -706,7 +707,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
},
{
'id': 125,
'name': 'saving backup',
'name': 'saving snapshot',
'serverRef': "http://localhost:8774/v1.1/servers/42",
'updated': self.NOW_API_FORMAT,
'created': self.NOW_API_FORMAT,
@ -729,7 +730,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
},
{
'id': 126,
'name': 'active backup',
'name': 'active snapshot',
'serverRef': "http://localhost:8774/v1.1/servers/42",
'updated': self.NOW_API_FORMAT,
'created': self.NOW_API_FORMAT,
@ -751,7 +752,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
},
{
'id': 127,
'name': 'killed backup',
'name': 'killed snapshot',
'serverRef': "http://localhost:8774/v1.1/servers/42",
'updated': self.NOW_API_FORMAT,
'created': self.NOW_API_FORMAT,
@ -802,7 +803,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
context = object()
filters = {'name': 'testname'}
image_service.index(
context, filters=filters, marker=0, limit=0).AndReturn([])
context, filters=filters).AndReturn([])
mocker.ReplayAll()
request = webob.Request.blank(
'/v1.1/images?name=testname')
@ -817,7 +818,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
context = object()
filters = {'status': 'ACTIVE'}
image_service.index(
context, filters=filters, marker=0, limit=0).AndReturn([])
context, filters=filters).AndReturn([])
mocker.ReplayAll()
request = webob.Request.blank(
'/v1.1/images?status=ACTIVE')
@ -832,7 +833,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
context = object()
filters = {'property-test': '3'}
image_service.index(
context, filters=filters, marker=0, limit=0).AndReturn([])
context, filters=filters).AndReturn([])
mocker.ReplayAll()
request = webob.Request.blank(
'/v1.1/images?property-test=3')
@ -847,7 +848,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
context = object()
filters = {'status': 'ACTIVE'}
image_service.index(
context, filters=filters, marker=0, limit=0).AndReturn([])
context, filters=filters).AndReturn([])
mocker.ReplayAll()
request = webob.Request.blank(
'/v1.1/images?status=ACTIVE&UNSUPPORTEDFILTER=testname')
@ -862,7 +863,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
context = object()
filters = {}
image_service.index(
context, filters=filters, marker=0, limit=0).AndReturn([])
context, filters=filters).AndReturn([])
mocker.ReplayAll()
request = webob.Request.blank(
'/v1.1/images')
@ -877,7 +878,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
context = object()
filters = {'name': 'testname'}
image_service.detail(
context, filters=filters, marker=0, limit=0).AndReturn([])
context, filters=filters).AndReturn([])
mocker.ReplayAll()
request = webob.Request.blank(
'/v1.1/images/detail?name=testname')
@ -892,7 +893,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
context = object()
filters = {'status': 'ACTIVE'}
image_service.detail(
context, filters=filters, marker=0, limit=0).AndReturn([])
context, filters=filters).AndReturn([])
mocker.ReplayAll()
request = webob.Request.blank(
'/v1.1/images/detail?status=ACTIVE')
@ -907,7 +908,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
context = object()
filters = {'property-test': '3'}
image_service.detail(
context, filters=filters, marker=0, limit=0).AndReturn([])
context, filters=filters).AndReturn([])
mocker.ReplayAll()
request = webob.Request.blank(
'/v1.1/images/detail?property-test=3')
@ -922,7 +923,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
context = object()
filters = {'status': 'ACTIVE'}
image_service.detail(
context, filters=filters, marker=0, limit=0).AndReturn([])
context, filters=filters).AndReturn([])
mocker.ReplayAll()
request = webob.Request.blank(
'/v1.1/images/detail?status=ACTIVE&UNSUPPORTEDFILTER=testname')
@ -937,7 +938,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
context = object()
filters = {}
image_service.detail(
context, filters=filters, marker=0, limit=0).AndReturn([])
context, filters=filters).AndReturn([])
mocker.ReplayAll()
request = webob.Request.blank(
'/v1.1/images/detail')
@ -969,8 +970,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
self.assertEqual(res.status_int, 404)
def test_create_image(self):
body = dict(image=dict(serverId='123', name='Backup 1'))
body = dict(image=dict(serverId='123', name='Snapshot 1'))
req = webob.Request.blank('/v1.0/images')
req.method = 'POST'
req.body = json.dumps(body)
@ -978,9 +978,95 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
response = req.get_response(fakes.wsgi_app())
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):
body = dict(image=dict(name='Backup 1'))
body = dict(image=dict(name='Snapshot 1'))
req = webob.Request.blank('/v1.0/images')
req.method = 'POST'
req.body = json.dumps(body)
@ -990,7 +1076,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
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.method = 'POST'
req.body = json.dumps(body)
@ -1024,7 +1110,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
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.method = 'POST'
req.body = json.dumps(body)
@ -1038,7 +1124,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
<image
created="None"
id="123"
name="Backup 1"
name="Snapshot 1"
serverRef="http://localhost/v1.1/servers/123"
status="ACTIVE"
updated="None"
@ -1057,7 +1143,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
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.method = 'POST'
req.body = json.dumps(body)
@ -1084,19 +1170,21 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
status='active', properties={})
image_id += 1
# Backup for User 1
# Snapshot for User 1
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'):
add_fixture(id=image_id, name='%s backup' % status,
add_fixture(id=image_id, name='%s snapshot' % status,
is_public=False, status=status,
properties=backup_properties)
properties=snapshot_properties)
image_id += 1
# Backup for User 2
other_backup_properties = {'instance_id': '43', 'user_id': '2'}
add_fixture(id=image_id, name='someone elses backup', is_public=False,
status='active', properties=other_backup_properties)
# Snapshot for User 2
other_snapshot_properties = {'instance_id': '43', 'user_id': '2'}
add_fixture(id=image_id, name='someone elses snapshot',
is_public=False, status='active',
properties=other_snapshot_properties)
image_id += 1
# Image without a name

View File

@ -37,6 +37,7 @@ from nova import log as logging
from nova import rpc
from nova import test
from nova import utils
from nova.notifier import test_notifier
LOG = logging.getLogger('nova.tests.compute')
FLAGS = flags.FLAGS
@ -62,6 +63,7 @@ class ComputeTestCase(test.TestCase):
super(ComputeTestCase, self).setUp()
self.flags(connection_type='fake',
stub_network=True,
notification_driver='nova.notifier.test_notifier',
network_manager='nova.network.manager.FlatManager')
self.compute = utils.import_object(FLAGS.compute_manager)
self.compute_api = compute.API()
@ -69,6 +71,7 @@ class ComputeTestCase(test.TestCase):
self.user = self.manager.create_user('fake', 'fake', 'fake')
self.project = self.manager.create_project('fake', 'fake', 'fake')
self.context = context.RequestContext('fake', 'fake', False)
test_notifier.NOTIFICATIONS = []
def fake_show(meh, context, id):
return {'id': 1, 'properties': {'kernel_id': 1, 'ramdisk_id': 1}}
@ -326,6 +329,50 @@ class ComputeTestCase(test.TestCase):
self.assert_(console)
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):
"""Ensure failure when running an instance that already exists"""
instance_id = self._create_instance()
@ -378,6 +425,36 @@ class ComputeTestCase(test.TestCase):
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):
"""Ensure instance can be migrated/resized"""
instance_id = self._create_instance()

View File

@ -274,6 +274,22 @@ EASIER_PASSWORD_SYMBOLS = ('23456789' # Removed: 0, 1
'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):
"""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 " -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 " -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 " -f, --force Force a clean re-build of the virtual environment. Useful when dependencies have been added."
echo " -p, --pep8 Just run pep8"
@ -25,6 +26,7 @@ function process_option {
-V|--virtual-env) let always_venv=1; let never_venv=0;;
-N|--no-virtual-env) let always_venv=0; let never_venv=1;;
-r|--recreate-db) let recreate_db=1;;
-n|--no-recreate-db) let recreate_db=0;;
-f|--force) let force=1;;
-p|--pep8) let just_pep8=1;;
-*) noseopts="$noseopts $1";;
@ -41,7 +43,7 @@ noseargs=
noseopts=
wrapper=""
just_pep8=0
recreate_db=0
recreate_db=1
for arg in "$@"; do
process_option $arg