merged trunk
This commit is contained in:
commit
51f64e0073
116
bin/instance-usage-audit
Executable file
116
bin/instance-usage-audit
Executable 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)
|
@ -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
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
@ -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])
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
@ -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()
|
||||||
|
@ -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
|
||||||
|
28
nova/notifier/test_notifier.py
Normal file
28
nova/notifier/test_notifier.py
Normal 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)
|
@ -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))
|
||||||
|
@ -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
|
||||||
|
@ -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})
|
||||||
|
@ -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
|
||||||
|
@ -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()
|
||||||
|
@ -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.
|
||||||
|
|
||||||
|
20
plugins/xenserver/xenapi/contrib/build-rpm.sh
Executable file
20
plugins/xenserver/xenapi/contrib/build-rpm.sh
Executable 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
|
@ -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
|
@ -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
|
||||||
|
Loading…
x
Reference in New Issue
Block a user