Skip to content

Commit f853e04

Browse files
Zuulopenstack-gerrit
authored andcommitted
Merge "Add online data migration for populating user_id"
2 parents b577b73 + a6bc544 commit f853e04

File tree

3 files changed

+257
-2
lines changed

3 files changed

+257
-2
lines changed

nova/cmd/manage.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -419,6 +419,8 @@ class DbCommands(object):
419419
compute_node_obj.migrate_empty_ratio,
420420
# Added in Stein
421421
virtual_interface_obj.fill_virtual_interface_list,
422+
# Added in Stein
423+
instance_mapping_obj.populate_user_id,
422424
)
423425

424426
def __init__(self):

nova/objects/instance_mapping.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,11 @@
1010
# License for the specific language governing permissions and limitations
1111
# under the License.
1212

13+
import collections
14+
1315
from oslo_log import log as logging
1416
from oslo_utils import versionutils
17+
import six
1518
from sqlalchemy.orm import joinedload
1619
from sqlalchemy.sql import false
1720
from sqlalchemy.sql import or_
@@ -25,6 +28,7 @@
2528
from nova.objects import base
2629
from nova.objects import cell_mapping
2730
from nova.objects import fields
31+
from nova.objects import virtual_interface
2832

2933

3034
LOG = logging.getLogger(__name__)
@@ -218,6 +222,73 @@ def populate_queued_for_delete(context, max_count):
218222
return processed, processed
219223

220224

225+
@db_api.api_context_manager.writer
226+
def populate_user_id(context, max_count):
227+
cells = objects.CellMappingList.get_all(context)
228+
cms_by_id = {cell.id: cell for cell in cells}
229+
done = 0
230+
unmigratable_ims = False
231+
ims = (
232+
# Get a list of instance mappings which do not have user_id populated.
233+
# We need to include records with queued_for_delete=True because they
234+
# include SOFT_DELETED instances, which could be restored at any time
235+
# in the future. If we don't migrate SOFT_DELETED instances now, we
236+
# wouldn't be able to retire this migration code later. Also filter
237+
# out the marker instance created by the virtual interface migration.
238+
context.session.query(api_models.InstanceMapping)
239+
.filter_by(user_id=None)
240+
.filter(api_models.InstanceMapping.project_id !=
241+
virtual_interface.FAKE_UUID)
242+
.limit(max_count).all())
243+
found = len(ims)
244+
ims_by_inst_uuid = {}
245+
inst_uuids_by_cell_id = collections.defaultdict(set)
246+
for im in ims:
247+
ims_by_inst_uuid[im.instance_uuid] = im
248+
inst_uuids_by_cell_id[im.cell_id].add(im.instance_uuid)
249+
for cell_id, inst_uuids in inst_uuids_by_cell_id.items():
250+
# We cannot migrate instance mappings that don't have a cell yet.
251+
if cell_id is None:
252+
unmigratable_ims = True
253+
continue
254+
with nova_context.target_cell(context, cms_by_id[cell_id]) as cctxt:
255+
# We need to migrate SOFT_DELETED instances because they could be
256+
# restored at any time in the future, preventing us from being able
257+
# to remove any other interim online data migration code we have,
258+
# if we don't migrate them here.
259+
# NOTE: it's not possible to query only for SOFT_DELETED instances.
260+
# We must query for both deleted and SOFT_DELETED instances.
261+
filters = {'uuid': inst_uuids}
262+
try:
263+
instances = objects.InstanceList.get_by_filters(
264+
cctxt, filters, expected_attrs=[])
265+
except Exception as exp:
266+
LOG.warning('Encountered exception: "%s" while querying '
267+
'instances from cell: %s. Continuing to the next '
268+
'cell.', six.text_type(exp),
269+
cms_by_id[cell_id].identity)
270+
continue
271+
# Walk through every instance that has a mapping needing to be updated
272+
# and update it.
273+
for instance in instances:
274+
im = ims_by_inst_uuid.pop(instance.uuid)
275+
im.user_id = instance.user_id
276+
context.session.add(im)
277+
done += 1
278+
if ims_by_inst_uuid:
279+
unmigratable_ims = True
280+
if done >= max_count:
281+
break
282+
283+
if unmigratable_ims:
284+
LOG.warning('Some instance mappings were not migratable. This may '
285+
'be transient due to in-flight instance builds, or could '
286+
'be due to stale data that will be cleaned up after '
287+
'running "nova-manage db archive_deleted_rows --purge".')
288+
289+
return found, done
290+
291+
221292
@base.NovaObjectRegistry.register
222293
class InstanceMappingList(base.ObjectListBase, base.NovaObject):
223294
# Version 1.0: Initial version

nova/tests/functional/db/test_instance_mapping.py

Lines changed: 184 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,15 @@
2020
from nova.objects import cell_mapping
2121
from nova.objects import instance
2222
from nova.objects import instance_mapping
23+
from nova.objects import virtual_interface
2324
from nova import test
2425
from nova.tests import fixtures
2526

2627

2728
sample_mapping = {'instance_uuid': '',
2829
'cell_id': 3,
29-
'project_id': 'fake-project'}
30+
'project_id': 'fake-project',
31+
'user_id': 'fake-user'}
3032

3133

3234
sample_cell_mapping = {'id': 3,
@@ -208,14 +210,194 @@ def test_populate_queued_for_delete(self):
208210

209211
def test_user_id_not_set_if_null_from_db(self):
210212
# Create an instance mapping with user_id=None.
211-
db_mapping = create_mapping()
213+
db_mapping = create_mapping(user_id=None)
212214
self.assertIsNone(db_mapping['user_id'])
213215
# Get the mapping to run convert from db object to versioned object.
214216
im = instance_mapping.InstanceMapping.get_by_instance_uuid(
215217
self.context, db_mapping['instance_uuid'])
216218
# Verify the user_id is not set.
217219
self.assertNotIn('user_id', im)
218220

221+
@mock.patch('nova.objects.instance_mapping.LOG.warning')
222+
def test_populate_user_id(self, mock_log_warning):
223+
cells = []
224+
celldbs = fixtures.CellDatabases()
225+
226+
# Create two cell databases and map them
227+
for uuid in (uuidsentinel.cell1, uuidsentinel.cell2):
228+
cm = cell_mapping.CellMapping(context=self.context, uuid=uuid,
229+
database_connection=uuid,
230+
transport_url='fake://')
231+
cm.create()
232+
cells.append(cm)
233+
celldbs.add_cell_database(uuid)
234+
self.useFixture(celldbs)
235+
236+
# Create 5 instances per cell
237+
for cell in cells:
238+
for i in range(0, 5):
239+
with context.target_cell(self.context, cell) as cctxt:
240+
inst = instance.Instance(
241+
cctxt,
242+
project_id=self.context.project_id,
243+
user_id=self.context.user_id)
244+
inst.create()
245+
# Make every other mapping have a NULL user_id
246+
# Will be a total of four mappings with NULL user_id
247+
user_id = self.context.user_id if i % 2 == 0 else None
248+
create_mapping(project_id=self.context.project_id,
249+
user_id=user_id, cell_id=cell.id,
250+
instance_uuid=inst.uuid)
251+
252+
# Create a SOFT_DELETED instance with a user_id=None instance mapping.
253+
# This should get migrated.
254+
with context.target_cell(self.context, cells[0]) as cctxt:
255+
inst = instance.Instance(
256+
cctxt, project_id=self.context.project_id,
257+
user_id=self.context.user_id, vm_state=vm_states.SOFT_DELETED)
258+
inst.create()
259+
create_mapping(project_id=self.context.project_id, user_id=None,
260+
cell_id=cells[0].id, instance_uuid=inst.uuid,
261+
queued_for_delete=True)
262+
263+
# Create a deleted instance with a user_id=None instance mapping.
264+
# This should get migrated.
265+
with context.target_cell(self.context, cells[1]) as cctxt:
266+
inst = instance.Instance(
267+
cctxt, project_id=self.context.project_id,
268+
user_id=self.context.user_id)
269+
inst.create()
270+
inst.destroy()
271+
create_mapping(project_id=self.context.project_id, user_id=None,
272+
cell_id=cells[1].id, instance_uuid=inst.uuid,
273+
queued_for_delete=True)
274+
275+
# Create an instance mapping for an instance not yet scheduled. It
276+
# should not get migrated because we won't know what user_id to use.
277+
unscheduled = create_mapping(project_id=self.context.project_id,
278+
user_id=None, cell_id=None)
279+
280+
# Create two instance mappings for instances that no longer exist.
281+
# Example: residue from a manual cleanup or after a periodic compute
282+
# purge and before a database archive. This record should not get
283+
# migrated.
284+
nonexistent = []
285+
for i in range(2):
286+
nonexistent.append(
287+
create_mapping(project_id=self.context.project_id,
288+
user_id=None, cell_id=cells[i].id,
289+
instance_uuid=uuidutils.generate_uuid()))
290+
291+
# Create an instance mapping simulating a virtual interface migration
292+
# marker instance which has had map_instances run on it.
293+
# This should not be found by the migration.
294+
create_mapping(project_id=virtual_interface.FAKE_UUID, user_id=None)
295+
296+
found, done = instance_mapping.populate_user_id(self.context, 2)
297+
# Two needed fixing, and honored the limit.
298+
self.assertEqual(2, found)
299+
self.assertEqual(2, done)
300+
301+
found, done = instance_mapping.populate_user_id(self.context, 1000)
302+
# Only four left were fixable. The fifth instance found has no
303+
# cell and cannot be migrated yet. The 6th and 7th instances found have
304+
# no corresponding instance records and cannot be migrated.
305+
self.assertEqual(7, found)
306+
self.assertEqual(4, done)
307+
308+
# Verify the orphaned instance mappings warning log message was only
309+
# emitted once.
310+
mock_log_warning.assert_called_once()
311+
312+
# Check that we have only the expected number of records with
313+
# user_id set. We created 10 instances (5 per cell with 2 per cell
314+
# with NULL user_id), 1 SOFT_DELETED instance with NULL user_id,
315+
# 1 deleted instance with NULL user_id, and 1 not-yet-scheduled
316+
# instance with NULL user_id.
317+
# We expect 12 of them to have user_id set after migration (15 total,
318+
# with the not-yet-scheduled instance and the orphaned instance
319+
# mappings ignored).
320+
ims = instance_mapping.InstanceMappingList.get_by_project_id(
321+
self.context, self.context.project_id)
322+
self.assertEqual(12, len(
323+
[im for im in ims if 'user_id' in im]))
324+
325+
# Check that one instance mapping record (not yet scheduled) has not
326+
# been migrated by this script.
327+
# Check that two other instance mapping records (no longer existing
328+
# instances) have not been migrated by this script.
329+
self.assertEqual(15, len(ims))
330+
331+
# Set the cell and create the instance for the mapping without a cell,
332+
# then run the migration again.
333+
unscheduled = instance_mapping.InstanceMapping.get_by_instance_uuid(
334+
self.context, unscheduled['instance_uuid'])
335+
unscheduled.cell_mapping = cells[0]
336+
unscheduled.save()
337+
with context.target_cell(self.context, cells[0]) as cctxt:
338+
inst = instance.Instance(
339+
cctxt,
340+
uuid=unscheduled.instance_uuid,
341+
project_id=self.context.project_id,
342+
user_id=self.context.user_id)
343+
inst.create()
344+
found, done = instance_mapping.populate_user_id(self.context, 1000)
345+
# Should have found the not-yet-scheduled instance and the orphaned
346+
# instance mappings.
347+
self.assertEqual(3, found)
348+
# Should have only migrated the not-yet-schedule instance.
349+
self.assertEqual(1, done)
350+
351+
# Delete the orphaned instance mapping (simulate manual cleanup by an
352+
# operator).
353+
for db_im in nonexistent:
354+
nonexist = instance_mapping.InstanceMapping.get_by_instance_uuid(
355+
self.context, db_im['instance_uuid'])
356+
nonexist.destroy()
357+
358+
# Run the script one last time to make sure it finds nothing left to
359+
# migrate.
360+
found, done = instance_mapping.populate_user_id(self.context, 1000)
361+
self.assertEqual(0, found)
362+
self.assertEqual(0, done)
363+
364+
@mock.patch('nova.objects.InstanceList.get_by_filters')
365+
def test_populate_user_id_instance_get_fail(self, mock_inst_get):
366+
cells = []
367+
celldbs = fixtures.CellDatabases()
368+
369+
# Create two cell databases and map them
370+
for uuid in (uuidsentinel.cell1, uuidsentinel.cell2):
371+
cm = cell_mapping.CellMapping(context=self.context, uuid=uuid,
372+
database_connection=uuid,
373+
transport_url='fake://')
374+
cm.create()
375+
cells.append(cm)
376+
celldbs.add_cell_database(uuid)
377+
self.useFixture(celldbs)
378+
379+
# Create one instance per cell
380+
for cell in cells:
381+
with context.target_cell(self.context, cell) as cctxt:
382+
inst = instance.Instance(
383+
cctxt,
384+
project_id=self.context.project_id,
385+
user_id=self.context.user_id)
386+
inst.create()
387+
create_mapping(project_id=self.context.project_id,
388+
user_id=None, cell_id=cell.id,
389+
instance_uuid=inst.uuid)
390+
391+
# Simulate the first cell is down/has some error
392+
mock_inst_get.side_effect = [test.TestingException(),
393+
instance.InstanceList(objects=[inst])]
394+
395+
found, done = instance_mapping.populate_user_id(self.context, 1000)
396+
# Verify we continue to the next cell when a down/error cell is
397+
# encountered.
398+
self.assertEqual(2, found)
399+
self.assertEqual(1, done)
400+
219401

220402
class InstanceMappingListTestCase(test.NoDBTestCase):
221403
USES_DB_SELF = True

0 commit comments

Comments
 (0)