Skip to content

Commit 97b8cb3

Browse files
Kevin_Zhengmelwitt
authored andcommitted
nova-manage db archive_deleted_rows is not multi-cell aware
The archive_deleted_rows cmd depend on DB connection config from config file, and when applying super-conductor mode, there are several config files for different cells. If so, the command can only archive rows in cell0 DB as it only reads the nova.conf This patch added code that provides --all-cells parameter to the command and read info for all cells from the api_db and then archive rows across all cells. The --all-cells parameter is passed on to the purge command when archive_deleted_rows is called with both --all-cells and --purge. Co-Authored-By: melanie witt <[email protected]> Change-Id: Id16c3d91d9ce5db9ffd125b59fffbfedf4a6843d Closes-Bug: #1719487
1 parent 90401df commit 97b8cb3

File tree

11 files changed

+514
-94
lines changed

11 files changed

+514
-94
lines changed

doc/source/cli/nova-manage.rst

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ Nova Database
6262
Returns exit code 0 if the database schema was synced successfully, or 1 if
6363
cell0 cannot be accessed.
6464

65-
``nova-manage db archive_deleted_rows [--max_rows <number>] [--verbose] [--until-complete] [--before <date>] [--purge]``
65+
``nova-manage db archive_deleted_rows [--max_rows <number>] [--verbose] [--until-complete] [--before <date>] [--purge] [--all-cells]``
6666
Move deleted rows from production tables to shadow tables. Note that the
6767
corresponding rows in the ``instance_mappings``, ``request_specs`` and
6868
``instance_group_member`` tables of the API database are purged when
@@ -78,7 +78,8 @@ Nova Database
7878
tables related to those instances. Specifying ``--purge`` will cause a
7979
*full* DB purge to be completed after archival. If a date range is desired
8080
for the purge, then run ``nova-manage db purge --before <date>`` manually
81-
after archiving is complete.
81+
after archiving is complete. Specifying ``--all-cells`` will
82+
cause the process to run against all cell databases.
8283

8384
**Return Codes**
8485

nova/cmd/manage.py

Lines changed: 132 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -414,17 +414,19 @@ def __init__(self):
414414
pass
415415

416416
@staticmethod
417-
def _print_dict(dct, dict_property="Property", dict_value='Value'):
417+
def _print_dict(dct, dict_property="Property", dict_value='Value',
418+
sort_key=None):
418419
"""Print a `dict` as a table of two columns.
419420
420421
:param dct: `dict` to print
421422
:param dict_property: name of the first column
422423
:param wrap: wrapping for the second column
423424
:param dict_value: header label for the value (second) column
425+
:param sort_key: key used for sorting the dict
424426
"""
425427
pt = prettytable.PrettyTable([dict_property, dict_value])
426428
pt.align = 'l'
427-
for k, v in sorted(dct.items()):
429+
for k, v in sorted(dct.items(), key=sort_key):
428430
# convert dict to str to check length
429431
if isinstance(v, dict):
430432
v = six.text_type(v)
@@ -495,9 +497,11 @@ def version(self):
495497
'max_rows as a batch size for each iteration.'))
496498
@args('--purge', action='store_true', dest='purge', default=False,
497499
help='Purge all data from shadow tables after archive completes')
500+
@args('--all-cells', action='store_true', dest='all_cells',
501+
default=False, help='Run command across all cells.')
498502
def archive_deleted_rows(self, max_rows=1000, verbose=False,
499503
until_complete=False, purge=False,
500-
before=None):
504+
before=None, all_cells=False):
501505
"""Move deleted rows from production tables to shadow tables.
502506
503507
Returns 0 if nothing was archived, 1 if some number of rows were
@@ -520,7 +524,7 @@ def archive_deleted_rows(self, max_rows=1000, verbose=False,
520524
# NOTE(tssurya): This check has been added to validate if the API
521525
# DB is reachable or not as this is essential for purging the
522526
# related API database records of the deleted instances.
523-
objects.CellMappingList.get_all(ctxt)
527+
cell_mappings = objects.CellMappingList.get_all(ctxt)
524528
except db_exc.CantStartEngineError:
525529
print(_('Failed to connect to API DB so aborting this archival '
526530
'attempt. Please check your config file to make sure that '
@@ -538,59 +542,146 @@ def archive_deleted_rows(self, max_rows=1000, verbose=False,
538542
before_date = None
539543

540544
table_to_rows_archived = {}
541-
deleted_instance_uuids = []
542545
if until_complete and verbose:
543546
sys.stdout.write(_('Archiving') + '..') # noqa
544-
while True:
545-
try:
546-
run, deleted_instance_uuids = db.archive_deleted_rows(
547-
max_rows, before=before_date)
548-
except KeyboardInterrupt:
549-
run = {}
550-
if until_complete and verbose:
551-
print('.' + _('stopped')) # noqa
552-
break
553-
for k, v in run.items():
554-
table_to_rows_archived.setdefault(k, 0)
555-
table_to_rows_archived[k] += v
556-
if deleted_instance_uuids:
557-
table_to_rows_archived.setdefault('instance_mappings', 0)
558-
table_to_rows_archived.setdefault('request_specs', 0)
559-
table_to_rows_archived.setdefault('instance_group_member', 0)
560-
deleted_mappings = objects.InstanceMappingList.destroy_bulk(
561-
ctxt, deleted_instance_uuids)
562-
table_to_rows_archived['instance_mappings'] += deleted_mappings
563-
deleted_specs = objects.RequestSpec.destroy_bulk(
564-
ctxt, deleted_instance_uuids)
565-
table_to_rows_archived['request_specs'] += deleted_specs
566-
deleted_group_members = (
567-
objects.InstanceGroup.destroy_members_bulk(
568-
ctxt, deleted_instance_uuids))
569-
table_to_rows_archived['instance_group_member'] += (
570-
deleted_group_members)
571-
if not until_complete:
572-
break
573-
elif not run:
574-
if verbose:
575-
print('.' + _('complete')) # noqa
547+
548+
interrupt = False
549+
550+
if all_cells:
551+
# Sort first by cell name, then by table:
552+
# +--------------------------------+-------------------------+
553+
# | Table | Number of Rows Archived |
554+
# +--------------------------------+-------------------------+
555+
# | cell0.block_device_mapping | 1 |
556+
# | cell1.block_device_mapping | 1 |
557+
# | cell1.instance_actions | 2 |
558+
# | cell1.instance_actions_events | 2 |
559+
# | cell2.block_device_mapping | 1 |
560+
# | cell2.instance_actions | 2 |
561+
# | cell2.instance_actions_events | 2 |
562+
# ...
563+
def sort_func(item):
564+
cell_name, table = item[0].split('.')
565+
return cell_name, table
566+
print_sort_func = sort_func
567+
else:
568+
cell_mappings = [None]
569+
print_sort_func = None
570+
total_rows_archived = 0
571+
for cell_mapping in cell_mappings:
572+
# NOTE(Kevin_Zheng): No need to calculate limit for each
573+
# cell if until_complete=True.
574+
# We need not adjust max rows to avoid exceeding a specified total
575+
# limit because with until_complete=True, we have no total limit.
576+
if until_complete:
577+
max_rows_to_archive = max_rows
578+
elif max_rows > total_rows_archived:
579+
# We reduce the max rows to archive based on what we've
580+
# archived so far to avoid potentially exceeding the specified
581+
# total limit.
582+
max_rows_to_archive = max_rows - total_rows_archived
583+
else:
576584
break
577-
if verbose:
578-
sys.stdout.write('.')
585+
# If all_cells=False, cell_mapping is None
586+
with context.target_cell(ctxt, cell_mapping) as cctxt:
587+
cell_name = cell_mapping.name if cell_mapping else None
588+
try:
589+
rows_archived = self._do_archive(
590+
table_to_rows_archived,
591+
cctxt,
592+
max_rows_to_archive,
593+
until_complete,
594+
verbose,
595+
before_date,
596+
cell_name)
597+
except KeyboardInterrupt:
598+
interrupt = True
599+
break
600+
# TODO(melwitt): Handle skip/warn for unreachable cells. Note
601+
# that cell_mappings = [None] if not --all-cells
602+
total_rows_archived += rows_archived
603+
604+
if until_complete and verbose:
605+
if interrupt:
606+
print('.' + _('stopped')) # noqa
607+
else:
608+
print('.' + _('complete')) # noqa
609+
579610
if verbose:
580611
if table_to_rows_archived:
581612
self._print_dict(table_to_rows_archived, _('Table'),
582-
dict_value=_('Number of Rows Archived'))
613+
dict_value=_('Number of Rows Archived'),
614+
sort_key=print_sort_func)
583615
else:
584616
print(_('Nothing was archived.'))
585617

586618
if table_to_rows_archived and purge:
587619
if verbose:
588620
print(_('Rows were archived, running purge...'))
589-
self.purge(purge_all=True, verbose=verbose)
621+
self.purge(purge_all=True, verbose=verbose, all_cells=all_cells)
590622

591623
# NOTE(danms): Return nonzero if we archived something
592624
return int(bool(table_to_rows_archived))
593625

626+
def _do_archive(self, table_to_rows_archived, cctxt, max_rows,
627+
until_complete, verbose, before_date, cell_name):
628+
"""Helper function for archiving deleted rows for a cell.
629+
630+
This will archive deleted rows for a cell database and remove the
631+
associated API database records for deleted instances.
632+
633+
:param table_to_rows_archived: Dict tracking the number of rows
634+
archived by <cell_name>.<table name>. Example:
635+
{'cell0.instances': 2,
636+
'cell1.instances': 5}
637+
:param cctxt: Cell-targeted nova.context.RequestContext if archiving
638+
across all cells
639+
:param max_rows: Maximum number of deleted rows to archive
640+
:param until_complete: Whether to run continuously until all deleted
641+
rows are archived
642+
:param verbose: Whether to print how many rows were archived per table
643+
:param before_date: Archive rows that were deleted before this date
644+
:param cell_name: Name of the cell or None if not archiving across all
645+
cells
646+
"""
647+
ctxt = context.get_admin_context()
648+
while True:
649+
run, deleted_instance_uuids, total_rows_archived = \
650+
db.archive_deleted_rows(cctxt, max_rows, before=before_date)
651+
for table_name, rows_archived in run.items():
652+
if cell_name:
653+
table_name = cell_name + '.' + table_name
654+
table_to_rows_archived.setdefault(table_name, 0)
655+
table_to_rows_archived[table_name] += rows_archived
656+
if deleted_instance_uuids:
657+
table_to_rows_archived.setdefault(
658+
'API_DB.instance_mappings', 0)
659+
table_to_rows_archived.setdefault(
660+
'API_DB.request_specs', 0)
661+
table_to_rows_archived.setdefault(
662+
'API_DB.instance_group_member', 0)
663+
deleted_mappings = objects.InstanceMappingList.destroy_bulk(
664+
ctxt, deleted_instance_uuids)
665+
table_to_rows_archived[
666+
'API_DB.instance_mappings'] += deleted_mappings
667+
deleted_specs = objects.RequestSpec.destroy_bulk(
668+
ctxt, deleted_instance_uuids)
669+
table_to_rows_archived[
670+
'API_DB.request_specs'] += deleted_specs
671+
deleted_group_members = (
672+
objects.InstanceGroup.destroy_members_bulk(
673+
ctxt, deleted_instance_uuids))
674+
table_to_rows_archived[
675+
'API_DB.instance_group_member'] += deleted_group_members
676+
# If we're not archiving until there is nothing more to archive, we
677+
# have reached max_rows in this cell DB or there was nothing to
678+
# archive.
679+
if not until_complete or not run:
680+
break
681+
if verbose:
682+
sys.stdout.write('.')
683+
return total_rows_archived
684+
594685
@args('--before', metavar='<before>', dest='before',
595686
help='If specified, purge rows from shadow tables that are older '
596687
'than this. Accepts date strings in the default format output '

nova/db/api.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1772,14 +1772,15 @@ def task_log_get(context, task_name, period_beginning,
17721772
####################
17731773

17741774

1775-
def archive_deleted_rows(max_rows=None, before=None):
1775+
def archive_deleted_rows(context=None, max_rows=None, before=None):
17761776
"""Move up to max_rows rows from production tables to the corresponding
17771777
shadow tables.
17781778
1779+
:param context: nova.context.RequestContext for database access
17791780
:param max_rows: Maximum number of rows to archive (required)
17801781
:param before: optional datetime which when specified filters the records
17811782
to only archive those records deleted before the given date
1782-
:returns: 2-item tuple:
1783+
:returns: 3-item tuple:
17831784
17841785
- dict that maps table name to number of rows archived from that table,
17851786
for example::
@@ -1790,8 +1791,10 @@ def archive_deleted_rows(max_rows=None, before=None):
17901791
'pci_devices': 2,
17911792
}
17921793
- list of UUIDs of instances that were archived
1794+
- total number of rows that were archived
17931795
"""
1794-
return IMPL.archive_deleted_rows(max_rows=max_rows, before=before)
1796+
return IMPL.archive_deleted_rows(context=context, max_rows=max_rows,
1797+
before=before)
17951798

17961799

17971800
def pcidevice_online_data_migration(context, max_count):

nova/db/sqlalchemy/api.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5569,14 +5569,15 @@ def _archive_deleted_rows_for_table(metadata, tablename, max_rows, before):
55695569
return rows_archived, deleted_instance_uuids
55705570

55715571

5572-
def archive_deleted_rows(max_rows=None, before=None):
5572+
def archive_deleted_rows(context=None, max_rows=None, before=None):
55735573
"""Move up to max_rows rows from production tables to the corresponding
55745574
shadow tables.
55755575
5576+
:param context: nova.context.RequestContext for database access
55765577
:param max_rows: Maximum number of rows to archive (required)
55775578
:param before: optional datetime which when specified filters the records
55785579
to only archive those records deleted before the given date
5579-
:returns: 2-item tuple:
5580+
:returns: 3-item tuple:
55805581
55815582
- dict that maps table name to number of rows archived from that table,
55825583
for example::
@@ -5587,11 +5588,12 @@ def archive_deleted_rows(max_rows=None, before=None):
55875588
'pci_devices': 2,
55885589
}
55895590
- list of UUIDs of instances that were archived
5591+
- total number of rows that were archived
55905592
"""
55915593
table_to_rows_archived = {}
55925594
deleted_instance_uuids = []
55935595
total_rows_archived = 0
5594-
meta = MetaData(get_engine(use_slave=True))
5596+
meta = MetaData(get_engine(use_slave=True, context=context))
55955597
meta.reflect()
55965598
# Reverse sort the tables so we get the leaf nodes first for processing.
55975599
for table in reversed(meta.sorted_tables):
@@ -5615,7 +5617,7 @@ def archive_deleted_rows(max_rows=None, before=None):
56155617
table_to_rows_archived[tablename] = rows_archived
56165618
if total_rows_archived >= max_rows:
56175619
break
5618-
return table_to_rows_archived, deleted_instance_uuids
5620+
return table_to_rows_archived, deleted_instance_uuids, total_rows_archived
56195621

56205622

56215623
def _purgeable_tables(metadata):

nova/tests/functional/db/test_archive.py

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,8 @@ def test_archive_deleted_rows(self):
9494
self.assertTrue(len(instance.system_metadata),
9595
'No system_metadata for instance: %s' % server_id)
9696
# Now try and archive the soft deleted records.
97-
results, deleted_instance_uuids = db.archive_deleted_rows(max_rows=100)
97+
results, deleted_instance_uuids, archived = \
98+
db.archive_deleted_rows(max_rows=100)
9899
# verify system_metadata was dropped
99100
self.assertIn('instance_system_metadata', results)
100101
self.assertEqual(len(instance.system_metadata),
@@ -105,6 +106,7 @@ def test_archive_deleted_rows(self):
105106
# by the archive
106107
self.assertIn('instance_actions', results)
107108
self.assertIn('instance_actions_events', results)
109+
self.assertEqual(sum(results.values()), archived)
108110

109111
def test_archive_deleted_rows_with_undeleted_residue(self):
110112
# Boots a server, deletes it, and then tries to archive it.
@@ -136,7 +138,8 @@ def test_archive_deleted_rows_with_undeleted_residue(self):
136138
self.assertTrue(len(instance.system_metadata),
137139
'No system_metadata for instance: %s' % server_id)
138140
# Now try and archive the soft deleted records.
139-
results, deleted_instance_uuids = db.archive_deleted_rows(max_rows=100)
141+
results, deleted_instance_uuids, archived = \
142+
db.archive_deleted_rows(max_rows=100)
140143
# verify system_metadata was dropped
141144
self.assertIn('instance_system_metadata', results)
142145
self.assertEqual(len(instance.system_metadata),
@@ -147,6 +150,7 @@ def test_archive_deleted_rows_with_undeleted_residue(self):
147150
# by the archive
148151
self.assertIn('instance_actions', results)
149152
self.assertIn('instance_actions_events', results)
153+
self.assertEqual(sum(results.values()), archived)
150154

151155
def _get_table_counts(self):
152156
engine = sqlalchemy_api.get_engine()
@@ -165,7 +169,7 @@ def test_archive_then_purge_all(self):
165169
server = self._create_server()
166170
server_id = server['id']
167171
self._delete_server(server_id)
168-
results, deleted_ids = db.archive_deleted_rows(max_rows=1000)
172+
results, deleted_ids, archived = db.archive_deleted_rows(max_rows=1000)
169173
self.assertEqual([server_id], deleted_ids)
170174

171175
lines = []
@@ -178,6 +182,7 @@ def status(msg):
178182
None, status_fn=status)
179183
self.assertNotEqual(0, deleted)
180184
self.assertNotEqual(0, len(lines))
185+
self.assertEqual(sum(results.values()), archived)
181186
for line in lines:
182187
self.assertIsNotNone(re.match(r'Deleted [1-9][0-9]* rows from .*',
183188
line))
@@ -190,8 +195,9 @@ def test_archive_then_purge_by_date(self):
190195
server = self._create_server()
191196
server_id = server['id']
192197
self._delete_server(server_id)
193-
results, deleted_ids = db.archive_deleted_rows(max_rows=1000)
198+
results, deleted_ids, archived = db.archive_deleted_rows(max_rows=1000)
194199
self.assertEqual([server_id], deleted_ids)
200+
self.assertEqual(sum(results.values()), archived)
195201

196202
pre_purge_results = self._get_table_counts()
197203

@@ -224,9 +230,10 @@ def test_purge_with_real_date(self):
224230
server = self._create_server()
225231
server_id = server['id']
226232
self._delete_server(server_id)
227-
results, deleted_ids = db.archive_deleted_rows(max_rows=1000)
233+
results, deleted_ids, archived = db.archive_deleted_rows(max_rows=1000)
228234
self.assertEqual([server_id], deleted_ids)
229235
date = dateutil_parser.parse('oct 21 2015', fuzzy=True)
230236
admin_context = context.get_admin_context()
231237
deleted = sqlalchemy_api.purge_shadow_tables(admin_context, date)
232238
self.assertEqual(0, deleted)
239+
self.assertEqual(sum(results.values()), archived)

0 commit comments

Comments
 (0)