diff --git a/doc/source/man/nova-manage.rst b/doc/source/man/nova-manage.rst index 81eace4d4f64..49fd3abb2f71 100644 --- a/doc/source/man/nova-manage.rst +++ b/doc/source/man/nova-manage.rst @@ -50,9 +50,11 @@ Nova Db Sync the main database up to the most recent version. This is the standard way to create the db as well. -``nova-manage db archive_deleted_rows [--max_rows ]`` +``nova-manage db archive_deleted_rows [--max_rows ] [--verbose]`` - Move deleted rows from production tables to shadow tables. + Move deleted rows from production tables to shadow tables. Specifying + --verbose will print the results of the archive operation for any tables + that were changed. ``nova-manage db null_instance_uuid_scan [--delete]`` diff --git a/nova/cmd/manage.py b/nova/cmd/manage.py index 9bca47d95820..501ad11e7123 100644 --- a/nova/cmd/manage.py +++ b/nova/cmd/manage.py @@ -940,7 +940,9 @@ class DbCommands(object): @args('--max_rows', metavar='', help='Maximum number of deleted rows to archive') - def archive_deleted_rows(self, max_rows): + @args('--verbose', action='store_true', dest='verbose', default=False, + help='Print how many rows were archived per table.') + def archive_deleted_rows(self, max_rows, verbose=False): """Move up to max_rows deleted rows from production tables to shadow tables. """ @@ -949,7 +951,13 @@ class DbCommands(object): if max_rows < 0: print(_("Must supply a positive value for max_rows")) return(1) - db.archive_deleted_rows(max_rows) + table_to_rows_archived = db.archive_deleted_rows(max_rows) + if verbose: + if table_to_rows_archived: + cliutils.print_dict(table_to_rows_archived, _('Table'), + dict_value=_('Number of Rows Archived')) + else: + print(_('Nothing was archived.')) @args('--delete', action='store_true', dest='delete', help='If specified, automatically delete any records found where ' diff --git a/nova/db/api.py b/nova/db/api.py index 47168ae75970..4e2d58bdadd8 100644 --- a/nova/db/api.py +++ b/nova/db/api.py @@ -1899,7 +1899,17 @@ def archive_deleted_rows(max_rows=None): """Move up to max_rows rows from production tables to corresponding shadow tables. - :returns: number of rows archived. + :returns: dict that maps table name to number of rows archived from that + table, for example: + + :: + + { + 'instances': 5, + 'block_device_mapping': 5, + 'pci_devices': 2, + } + """ return IMPL.archive_deleted_rows(max_rows=max_rows) diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index 38b0a276fce1..ed89865984d1 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -6035,19 +6035,34 @@ def archive_deleted_rows(max_rows=None): """Move up to max_rows rows from production tables to the corresponding shadow tables. - :returns: Number of rows archived. + :returns: dict that maps table name to number of rows archived from that + table, for example: + + :: + + { + 'instances': 5, + 'block_device_mapping': 5, + 'pci_devices': 2, + } + """ + table_to_rows_archived = {} tablenames = [] for model_class in six.itervalues(models.__dict__): if hasattr(model_class, "__tablename__"): tablenames.append(model_class.__tablename__) - rows_archived = 0 + total_rows_archived = 0 for tablename in tablenames: - rows_archived += _archive_deleted_rows_for_table(tablename, - max_rows=max_rows - rows_archived) - if rows_archived >= max_rows: + rows_archived = _archive_deleted_rows_for_table( + tablename, max_rows=max_rows - total_rows_archived) + total_rows_archived += rows_archived + # Only report results for tables that had updates. + if rows_archived: + table_to_rows_archived[tablename] = rows_archived + if total_rows_archived >= max_rows: break - return rows_archived + return table_to_rows_archived #################### diff --git a/nova/tests/unit/db/test_db_api.py b/nova/tests/unit/db/test_db_api.py index 0f154155aaa0..af7f6117321a 100644 --- a/nova/tests/unit/db/test_db_api.py +++ b/nova/tests/unit/db/test_db_api.py @@ -8022,7 +8022,7 @@ class Ec2TestCase(test.TestCase): self.ctxt, 100500) -class ArchiveTestCase(test.TestCase): +class ArchiveTestCase(test.TestCase, ModelsObjectComparatorMixin): def setUp(self): super(ArchiveTestCase, self).setUp() @@ -8107,7 +8107,9 @@ class ArchiveTestCase(test.TestCase): # Verify we have 0 in shadow self.assertEqual(len(rows), 0) # Archive 2 rows - db.archive_deleted_rows(max_rows=2) + results = db.archive_deleted_rows(max_rows=2) + expected = dict(instance_id_mappings=2) + self._assertEqualObjects(expected, results) rows = self.conn.execute(qiim).fetchall() # Verify we have 4 left in main self.assertEqual(len(rows), 4) @@ -8115,7 +8117,9 @@ class ArchiveTestCase(test.TestCase): # Verify we have 2 in shadow self.assertEqual(len(rows), 2) # Archive 2 more rows - db.archive_deleted_rows(max_rows=2) + results = db.archive_deleted_rows(max_rows=2) + expected = dict(instance_id_mappings=2) + self._assertEqualObjects(expected, results) rows = self.conn.execute(qiim).fetchall() # Verify we have 2 left in main self.assertEqual(len(rows), 2) @@ -8123,7 +8127,9 @@ class ArchiveTestCase(test.TestCase): # Verify we have 4 in shadow self.assertEqual(len(rows), 4) # Try to archive more, but there are no deleted rows left. - db.archive_deleted_rows(max_rows=2) + results = db.archive_deleted_rows(max_rows=2) + expected = dict() + self._assertEqualObjects(expected, results) rows = self.conn.execute(qiim).fetchall() # Verify we still have 2 left in main self.assertEqual(len(rows), 2) diff --git a/nova/tests/unit/test_nova_manage.py b/nova/tests/unit/test_nova_manage.py index 1be28fd22459..ac8936a69f1d 100644 --- a/nova/tests/unit/test_nova_manage.py +++ b/nova/tests/unit/test_nova_manage.py @@ -375,7 +375,7 @@ class VmCommandsTestCase(test.TestCase): self.assertIn('fake-host', result) -class DBCommandsTestCase(test.TestCase): +class DBCommandsTestCase(test.NoDBTestCase): def setUp(self): super(DBCommandsTestCase, self).setUp() self.commands = manage.DbCommands() @@ -383,6 +383,42 @@ class DBCommandsTestCase(test.TestCase): def test_archive_deleted_rows_negative(self): self.assertEqual(1, self.commands.archive_deleted_rows(-1)) + @mock.patch.object(db, 'archive_deleted_rows', + return_value=dict(instances=10, consoles=5)) + def _test_archive_deleted_rows(self, mock_db_archive, verbose=False): + self.useFixture(fixtures.MonkeyPatch('sys.stdout', StringIO())) + self.commands.archive_deleted_rows(20, verbose=verbose) + mock_db_archive.assert_called_once_with(20) + output = sys.stdout.getvalue() + if verbose: + expected = '''\ ++-----------+-------------------------+ +| Table | Number of Rows Archived | ++-----------+-------------------------+ +| consoles | 5 | +| instances | 10 | ++-----------+-------------------------+ +''' + self.assertEqual(expected, output) + else: + self.assertEqual(0, len(output)) + + def test_archive_deleted_rows(self): + # Tests that we don't show any table output (not verbose). + self._test_archive_deleted_rows() + + def test_archive_deleted_rows_verbose(self): + # Tests that we get table output. + self._test_archive_deleted_rows(verbose=True) + + @mock.patch.object(db, 'archive_deleted_rows', return_value={}) + def test_archive_deleted_rows_verbose_no_results(self, mock_db_archive): + self.useFixture(fixtures.MonkeyPatch('sys.stdout', StringIO())) + self.commands.archive_deleted_rows(20, verbose=True) + mock_db_archive.assert_called_once_with(20) + output = sys.stdout.getvalue() + self.assertIn('Nothing was archived.', output) + @mock.patch.object(migration, 'db_null_instance_uuid_scan', return_value={'foo': 0}) def test_null_instance_uuid_scan_no_records_found(self, mock_scan):