562 lines
24 KiB
Python
562 lines
24 KiB
Python
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
# not use this file except in compliance with the License. You may obtain
|
|
# a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
# License for the specific language governing permissions and limitations
|
|
# under the License.
|
|
|
|
import datetime
|
|
|
|
from oslo_utils.fixture import uuidsentinel as uuids
|
|
|
|
from nova.compute import instance_list
|
|
from nova import context
|
|
from nova.db import api as db
|
|
from nova import exception
|
|
from nova import objects
|
|
from nova import test
|
|
|
|
|
|
class InstanceListTestCase(test.TestCase):
|
|
NUMBER_OF_CELLS = 3
|
|
|
|
def setUp(self):
|
|
super(InstanceListTestCase, self).setUp()
|
|
|
|
self.context = context.RequestContext('fake', 'fake')
|
|
self.num_instances = 3
|
|
self.instances = []
|
|
|
|
start = datetime.datetime(1985, 10, 25, 1, 21, 0)
|
|
dt = start
|
|
spread = datetime.timedelta(minutes=10)
|
|
|
|
self.cells = objects.CellMappingList.get_all(self.context)
|
|
# Create three instances in each of the real cells. Leave the
|
|
# first cell empty to make sure we don't break with an empty
|
|
# one.
|
|
for cell in self.cells[1:]:
|
|
for i in range(0, self.num_instances):
|
|
with context.target_cell(self.context, cell) as cctx:
|
|
inst = objects.Instance(
|
|
context=cctx,
|
|
project_id=self.context.project_id,
|
|
user_id=self.context.user_id,
|
|
created_at=start,
|
|
launched_at=dt,
|
|
instance_type_id=i,
|
|
hostname='%s-inst%i' % (cell.name, i))
|
|
inst.create()
|
|
if i % 2 == 0:
|
|
# Make some faults for this instance
|
|
for n in range(0, i + 1):
|
|
msg = 'fault%i-%s' % (n, inst.hostname)
|
|
f = objects.InstanceFault(context=cctx,
|
|
instance_uuid=inst.uuid,
|
|
code=i,
|
|
message=msg,
|
|
details='fake',
|
|
host='fakehost')
|
|
f.create()
|
|
|
|
self.instances.append(inst)
|
|
im = objects.InstanceMapping(context=self.context,
|
|
project_id=inst.project_id,
|
|
user_id=inst.user_id,
|
|
instance_uuid=inst.uuid,
|
|
cell_mapping=cell)
|
|
im.create()
|
|
dt += spread
|
|
|
|
def test_get_sorted(self):
|
|
filters = {}
|
|
limit = None
|
|
marker = None
|
|
columns = []
|
|
sort_keys = ['uuid']
|
|
sort_dirs = ['asc']
|
|
obj, insts = instance_list.get_instances_sorted(self.context, filters,
|
|
limit, marker, columns,
|
|
sort_keys, sort_dirs)
|
|
uuids = [inst['uuid'] for inst in insts]
|
|
self.assertEqual(sorted(uuids), uuids)
|
|
self.assertEqual(len(self.instances), len(uuids))
|
|
|
|
def test_get_sorted_descending(self):
|
|
filters = {}
|
|
limit = None
|
|
marker = None
|
|
columns = []
|
|
sort_keys = ['uuid']
|
|
sort_dirs = ['desc']
|
|
obj, insts = instance_list.get_instances_sorted(self.context, filters,
|
|
limit, marker, columns,
|
|
sort_keys, sort_dirs)
|
|
uuids = [inst['uuid'] for inst in insts]
|
|
self.assertEqual(list(reversed(sorted(uuids))), uuids)
|
|
self.assertEqual(len(self.instances), len(uuids))
|
|
|
|
def test_get_sorted_with_filter(self):
|
|
filters = {'instance_type_id': 1}
|
|
limit = None
|
|
marker = None
|
|
columns = []
|
|
sort_keys = ['uuid']
|
|
sort_dirs = ['asc']
|
|
obj, insts = instance_list.get_instances_sorted(self.context, filters,
|
|
limit, marker, columns,
|
|
sort_keys, sort_dirs)
|
|
uuids = [inst['uuid'] for inst in insts]
|
|
expected = [inst['uuid'] for inst in self.instances
|
|
if inst['instance_type_id'] == 1]
|
|
self.assertEqual(list(sorted(expected)), uuids)
|
|
|
|
def test_get_sorted_by_defaults(self):
|
|
filters = {}
|
|
limit = None
|
|
marker = None
|
|
columns = []
|
|
sort_keys = None
|
|
sort_dirs = None
|
|
obj, insts = instance_list.get_instances_sorted(self.context, filters,
|
|
limit, marker, columns,
|
|
sort_keys, sort_dirs)
|
|
uuids = set([inst['uuid'] for inst in insts])
|
|
expected = set([inst['uuid'] for inst in self.instances])
|
|
self.assertEqual(expected, uuids)
|
|
|
|
def test_get_sorted_with_limit(self):
|
|
obj, insts = instance_list.get_instances_sorted(self.context, {},
|
|
5, None,
|
|
[], ['uuid'], ['asc'])
|
|
uuids = [inst['uuid'] for inst in insts]
|
|
had_uuids = [inst.uuid for inst in self.instances]
|
|
self.assertEqual(sorted(had_uuids)[:5], uuids)
|
|
self.assertEqual(5, len(uuids))
|
|
|
|
def test_get_sorted_with_large_limit(self):
|
|
obj, insts = instance_list.get_instances_sorted(self.context, {},
|
|
5000, None,
|
|
[], ['uuid'], ['asc'])
|
|
uuids = [inst['uuid'] for inst in insts]
|
|
self.assertEqual(sorted(uuids), uuids)
|
|
self.assertEqual(len(self.instances), len(uuids))
|
|
|
|
def test_get_sorted_with_large_limit_batched(self):
|
|
obj, insts = instance_list.get_instances_sorted(self.context, {},
|
|
5000, None,
|
|
[], ['uuid'], ['asc'],
|
|
batch_size=2)
|
|
uuids = [inst['uuid'] for inst in insts]
|
|
self.assertEqual(sorted(uuids), uuids)
|
|
self.assertEqual(len(self.instances), len(uuids))
|
|
|
|
def _test_get_sorted_with_limit_marker(self, sort_by, pages=2, pagesize=2,
|
|
sort_dir='asc'):
|
|
"""Get multiple pages by a sort key and validate the results.
|
|
|
|
This requests $pages of $pagesize, followed by a final page with
|
|
no limit, and a final-final page which should be empty. It validates
|
|
that we got a consistent set of results no patter where the page
|
|
boundary is, that we got all the results after the unlimited query,
|
|
and that the final page comes back empty when we use the last
|
|
instance as a marker.
|
|
"""
|
|
insts = []
|
|
|
|
page = 0
|
|
while True:
|
|
if page >= pages:
|
|
# We've requested the specified number of limited (by pagesize)
|
|
# pages, so request a penultimate page with no limit which
|
|
# should always finish out the result.
|
|
limit = None
|
|
else:
|
|
# Request a limited-size page for the first $pages pages.
|
|
limit = pagesize
|
|
|
|
if insts:
|
|
# If we're not on the first page, use the last instance we
|
|
# received as the marker
|
|
marker = insts[-1]['uuid']
|
|
else:
|
|
# No marker for the first page
|
|
marker = None
|
|
|
|
batch = list(
|
|
instance_list.get_instances_sorted(self.context, {},
|
|
limit, marker,
|
|
[], [sort_by],
|
|
[sort_dir])[1])
|
|
if not batch:
|
|
# This should only happen when we've pulled the last empty
|
|
# page because we used the marker of the last instance. If
|
|
# we end up with a non-deterministic ordering, we'd loop
|
|
# forever.
|
|
break
|
|
insts.extend(batch)
|
|
page += 1
|
|
if page > len(self.instances) * 2:
|
|
# Do this sanity check in case we introduce (or find) another
|
|
# repeating page bug like #1721791. Without this we loop
|
|
# until timeout, which is less obvious.
|
|
raise Exception('Infinite paging loop')
|
|
|
|
# We should have requested exactly (or one more unlimited) pages
|
|
self.assertIn(page, (pages, pages + 1))
|
|
|
|
# Make sure the full set matches what we know to be true
|
|
found = [x[sort_by] for x in insts]
|
|
had = [x[sort_by] for x in self.instances]
|
|
|
|
if sort_by in ('launched_at', 'created_at'):
|
|
# We're comparing objects and database entries, so we need to
|
|
# squash the tzinfo of the object ones so we can compare
|
|
had = [x.replace(tzinfo=None) for x in had]
|
|
|
|
self.assertEqual(len(had), len(found))
|
|
if sort_dir == 'asc':
|
|
self.assertEqual(sorted(had), found)
|
|
else:
|
|
self.assertEqual(list(reversed(sorted(had))), found)
|
|
|
|
def test_get_sorted_with_limit_marker_stable(self):
|
|
"""Test sorted by hostname.
|
|
|
|
This will be a stable sort that won't change on each run.
|
|
"""
|
|
self._test_get_sorted_with_limit_marker(sort_by='hostname')
|
|
|
|
def test_get_sorted_with_limit_marker_stable_reverse(self):
|
|
"""Test sorted by hostname.
|
|
|
|
This will be a stable sort that won't change on each run.
|
|
"""
|
|
self._test_get_sorted_with_limit_marker(sort_by='hostname',
|
|
sort_dir='desc')
|
|
|
|
def test_get_sorted_with_limit_marker_stable_different_pages(self):
|
|
"""Test sorted by hostname with different page sizes.
|
|
|
|
Just do the above with page seams in different places.
|
|
"""
|
|
self._test_get_sorted_with_limit_marker(sort_by='hostname',
|
|
pages=3, pagesize=1)
|
|
|
|
def test_get_sorted_with_limit_marker_stable_different_pages_reverse(self):
|
|
"""Test sorted by hostname with different page sizes.
|
|
|
|
Just do the above with page seams in different places.
|
|
"""
|
|
self._test_get_sorted_with_limit_marker(sort_by='hostname',
|
|
pages=3, pagesize=1,
|
|
sort_dir='desc')
|
|
|
|
def test_get_sorted_with_limit_marker_random(self):
|
|
"""Test sorted by uuid.
|
|
|
|
This will not be stable and the actual ordering will depend on
|
|
uuid generation and thus be different on each run. Do this in
|
|
addition to the stable sort above to keep us honest.
|
|
"""
|
|
self._test_get_sorted_with_limit_marker(sort_by='uuid')
|
|
|
|
def test_get_sorted_with_limit_marker_random_different_pages(self):
|
|
"""Test sorted by uuid with different page sizes.
|
|
|
|
Just do the above with page seams in different places.
|
|
"""
|
|
self._test_get_sorted_with_limit_marker(sort_by='uuid',
|
|
pages=3, pagesize=2)
|
|
|
|
def test_get_sorted_with_limit_marker_datetime(self):
|
|
"""Test sorted by launched_at.
|
|
|
|
This tests that we can do all of this, but with datetime
|
|
fields.
|
|
"""
|
|
self._test_get_sorted_with_limit_marker(sort_by='launched_at')
|
|
|
|
def test_get_sorted_with_limit_marker_datetime_same(self):
|
|
"""Test sorted by created_at.
|
|
|
|
This tests that we can do all of this, but with datetime
|
|
fields that are identical.
|
|
"""
|
|
self._test_get_sorted_with_limit_marker(sort_by='created_at')
|
|
|
|
def test_get_sorted_with_deleted_marker(self):
|
|
marker = self.instances[1]['uuid']
|
|
|
|
before = list(
|
|
instance_list.get_instances_sorted(self.context, {},
|
|
None, marker,
|
|
[], None, None)[1])
|
|
|
|
db.instance_destroy(self.context, marker)
|
|
|
|
after = list(
|
|
instance_list.get_instances_sorted(self.context, {},
|
|
None, marker,
|
|
[], None, None)[1])
|
|
|
|
self.assertEqual(before, after)
|
|
|
|
def test_get_sorted_with_invalid_marker(self):
|
|
self.assertRaises(exception.MarkerNotFound,
|
|
list, instance_list.get_instances_sorted(
|
|
self.context, {}, None, 'not-a-marker',
|
|
[], None, None)[1])
|
|
|
|
def test_get_sorted_with_purged_instance(self):
|
|
"""Test that we handle a mapped but purged instance."""
|
|
im = objects.InstanceMapping(self.context,
|
|
instance_uuid=uuids.missing,
|
|
project_id=self.context.project_id,
|
|
user_id=self.context.user_id,
|
|
cell=self.cells[0])
|
|
im.create()
|
|
self.assertRaises(exception.MarkerNotFound,
|
|
list, instance_list.get_instances_sorted(
|
|
self.context, {}, None, uuids.missing,
|
|
[], None, None)[1])
|
|
|
|
def _test_get_paginated_with_filter(self, filters):
|
|
|
|
found_uuids = []
|
|
marker = None
|
|
while True:
|
|
# Query for those instances, sorted by a different key in
|
|
# pages of one until we've consumed them all
|
|
batch = list(
|
|
instance_list.get_instances_sorted(self.context,
|
|
filters,
|
|
1, marker, [],
|
|
['hostname'],
|
|
['asc'])[1])
|
|
if not batch:
|
|
break
|
|
found_uuids.extend([x['uuid'] for x in batch])
|
|
marker = found_uuids[-1]
|
|
|
|
return found_uuids
|
|
|
|
def test_get_paginated_with_uuid_filter(self):
|
|
"""Test getting pages with uuid filters.
|
|
|
|
This runs through the results of a uuid-filtered query in pages of
|
|
length one to ensure that we land on markers that are filtered out
|
|
of the query and are not accidentally returned.
|
|
"""
|
|
# Pick a set of the instances by uuid, when sorted by uuid
|
|
all_uuids = [x['uuid'] for x in self.instances]
|
|
filters = {'uuid': sorted(all_uuids)[:7]}
|
|
|
|
found_uuids = self._test_get_paginated_with_filter(filters)
|
|
|
|
# Make sure we found all (and only) the instances we asked for
|
|
self.assertEqual(set(found_uuids), set(filters['uuid']))
|
|
self.assertEqual(7, len(found_uuids))
|
|
|
|
def test_get_paginated_with_other_filter(self):
|
|
"""Test getting pages with another filter.
|
|
|
|
This runs through the results of a filtered query in pages of
|
|
length one to ensure we land on markers that are filtered out
|
|
of the query and are not accidentally returned.
|
|
"""
|
|
expected = [inst['uuid'] for inst in self.instances
|
|
if inst['instance_type_id'] == 1]
|
|
filters = {'instance_type_id': 1}
|
|
|
|
found_uuids = self._test_get_paginated_with_filter(filters)
|
|
|
|
self.assertEqual(set(expected), set(found_uuids))
|
|
|
|
def test_get_paginated_with_uuid_and_other_filter(self):
|
|
"""Test getting pages with a uuid and other type of filter.
|
|
|
|
We do this to make sure that we still find (but exclude) the
|
|
marker even if one of the other filters would have included
|
|
it.
|
|
"""
|
|
# Pick a set of the instances by uuid, when sorted by uuid
|
|
all_uuids = [x['uuid'] for x in self.instances]
|
|
filters = {'uuid': sorted(all_uuids)[:7],
|
|
'user_id': 'fake'}
|
|
|
|
found_uuids = self._test_get_paginated_with_filter(filters)
|
|
|
|
# Make sure we found all (and only) the instances we asked for
|
|
self.assertEqual(set(found_uuids), set(filters['uuid']))
|
|
self.assertEqual(7, len(found_uuids))
|
|
|
|
def test_get_sorted_with_faults(self):
|
|
"""Make sure we get faults when we ask for them."""
|
|
insts = list(
|
|
instance_list.get_instances_sorted(self.context, {},
|
|
None, None,
|
|
['fault'],
|
|
['hostname'], ['asc'])[1])
|
|
|
|
# Two of the instances in each cell have faults (0th and 2nd)
|
|
expected_faults = self.NUMBER_OF_CELLS * 2
|
|
expected_no_fault = len(self.instances) - expected_faults
|
|
faults = [inst['fault'] for inst in insts]
|
|
self.assertEqual(expected_no_fault, faults.count(None))
|
|
|
|
def test_get_sorted_paginated_with_faults(self):
|
|
"""Get pages of one with faults.
|
|
|
|
Do this specifically so we make sure we land on faulted marker
|
|
instances to ensure we don't omit theirs.
|
|
"""
|
|
insts = []
|
|
while True:
|
|
if insts:
|
|
marker = insts[-1]['uuid']
|
|
else:
|
|
marker = None
|
|
batch = list(
|
|
instance_list.get_instances_sorted(self.context, {},
|
|
1, marker,
|
|
['fault'],
|
|
['hostname'], ['asc'])[1])
|
|
if not batch:
|
|
break
|
|
insts.extend(batch)
|
|
|
|
self.assertEqual(len(self.instances), len(insts))
|
|
# Two of the instances in each cell have faults (0th and 2nd)
|
|
expected_faults = self.NUMBER_OF_CELLS * 2
|
|
expected_no_fault = len(self.instances) - expected_faults
|
|
faults = [inst['fault'] for inst in insts]
|
|
self.assertEqual(expected_no_fault, faults.count(None))
|
|
|
|
def test_instance_list_minimal_cells(self):
|
|
"""Get a list of instances with a subset of cell mappings."""
|
|
last_cell = self.cells[-1]
|
|
with context.target_cell(self.context, last_cell) as cctxt:
|
|
last_cell_instances = db.instance_get_all(cctxt)
|
|
last_cell_uuids = [inst['uuid'] for inst in last_cell_instances]
|
|
|
|
instances = list(
|
|
instance_list.get_instances_sorted(self.context, {},
|
|
None, None, [],
|
|
['uuid'], ['asc'],
|
|
cell_mappings=self.cells[:-1])
|
|
[1])
|
|
found_uuids = [inst['hostname'] for inst in instances]
|
|
had_uuids = [inst['hostname'] for inst in self.instances
|
|
if inst['uuid'] not in last_cell_uuids]
|
|
self.assertEqual(sorted(had_uuids), sorted(found_uuids))
|
|
|
|
|
|
class TestInstanceListObjects(test.TestCase):
|
|
def setUp(self):
|
|
super(TestInstanceListObjects, self).setUp()
|
|
|
|
self.context = context.RequestContext('fake', 'fake')
|
|
self.num_instances = 3
|
|
self.instances = []
|
|
|
|
start = datetime.datetime(1985, 10, 25, 1, 21, 0)
|
|
dt = start
|
|
spread = datetime.timedelta(minutes=10)
|
|
|
|
cells = objects.CellMappingList.get_all(self.context)
|
|
# Create three instances in each of the real cells. Leave the
|
|
# first cell empty to make sure we don't break with an empty
|
|
# one
|
|
for cell in cells[1:]:
|
|
for i in range(0, self.num_instances):
|
|
with context.target_cell(self.context, cell) as cctx:
|
|
inst = objects.Instance(
|
|
context=cctx,
|
|
project_id=self.context.project_id,
|
|
user_id=self.context.user_id,
|
|
created_at=start,
|
|
launched_at=dt,
|
|
instance_type_id=i,
|
|
hostname='%s-inst%i' % (cell.name, i))
|
|
inst.create()
|
|
if i % 2 == 0:
|
|
# Make some faults for this instance
|
|
for n in range(0, i + 1):
|
|
msg = 'fault%i-%s' % (n, inst.hostname)
|
|
f = objects.InstanceFault(context=cctx,
|
|
instance_uuid=inst.uuid,
|
|
code=i,
|
|
message=msg,
|
|
details='fake',
|
|
host='fakehost')
|
|
f.create()
|
|
|
|
self.instances.append(inst)
|
|
im = objects.InstanceMapping(context=self.context,
|
|
project_id=inst.project_id,
|
|
user_id=inst.user_id,
|
|
instance_uuid=inst.uuid,
|
|
cell_mapping=cell)
|
|
im.create()
|
|
dt += spread
|
|
|
|
def test_get_instance_objects_sorted(self):
|
|
filters = {}
|
|
limit = None
|
|
marker = None
|
|
expected_attrs = []
|
|
sort_keys = ['uuid']
|
|
sort_dirs = ['asc']
|
|
insts, down_cell_uuids = instance_list.get_instance_objects_sorted(
|
|
self.context, filters, limit, marker, expected_attrs,
|
|
sort_keys, sort_dirs)
|
|
found_uuids = [x.uuid for x in insts]
|
|
had_uuids = sorted([x['uuid'] for x in self.instances])
|
|
self.assertEqual(had_uuids, found_uuids)
|
|
|
|
# Make sure none of the instances have fault set
|
|
self.assertEqual(0, len([inst for inst in insts
|
|
if 'fault' in inst]))
|
|
|
|
def test_get_instance_objects_sorted_with_fault(self):
|
|
filters = {}
|
|
limit = None
|
|
marker = None
|
|
expected_attrs = ['fault']
|
|
sort_keys = ['uuid']
|
|
sort_dirs = ['asc']
|
|
insts, down_cell_uuids = instance_list.get_instance_objects_sorted(
|
|
self.context, filters, limit, marker, expected_attrs,
|
|
sort_keys, sort_dirs)
|
|
found_uuids = [x.uuid for x in insts]
|
|
had_uuids = sorted([x['uuid'] for x in self.instances])
|
|
self.assertEqual(had_uuids, found_uuids)
|
|
|
|
# They should all have fault set, but only some have
|
|
# actual faults
|
|
self.assertEqual(2, len([inst for inst in insts
|
|
if inst.fault]))
|
|
|
|
def test_get_instance_objects_sorted_paged(self):
|
|
"""Query a full first page and ensure an empty second one.
|
|
|
|
This uses created_at which is enforced to be the same across
|
|
each instance by setUp(). This will help make sure we still
|
|
have a stable ordering, even when we only claim to care about
|
|
created_at.
|
|
"""
|
|
instp1, down_cell_uuids = instance_list.get_instance_objects_sorted(
|
|
self.context, {}, None, None, [],
|
|
['created_at'], ['asc'])
|
|
self.assertEqual(len(self.instances), len(instp1))
|
|
instp2, down_cell_uuids = instance_list.get_instance_objects_sorted(
|
|
self.context, {}, None, instp1[-1]['uuid'], [],
|
|
['created_at'], ['asc'])
|
|
self.assertEqual(0, len(instp2))
|