From e37279b4597371fc5c362455edeb1cf7760fa728 Mon Sep 17 00:00:00 2001 From: Matt Riedemann Date: Thu, 8 Oct 2015 08:55:55 -0700 Subject: [PATCH] Print number of rows archived per table in db archive_deleted_rows The `nova-manage db archive_deleted_rows` command doesn't print any results, not even how many rows were deleted, which is something the database API returns. The number of rows deleted also changes depending on the max_rows input and how many rows are deleted in each table, it's a cumulative effect. We should keep track of which tables we've deleted rows from along with how many rows, and print that all out to the CLI console if the user passed a --verbose option. Implements blueprint print-table-archived-rows Change-Id: I5c47cd5633eca056f8ae508753a41e2c1ed9e523 --- doc/source/man/nova-manage.rst | 6 +++-- nova/cmd/manage.py | 12 +++++++-- nova/db/api.py | 12 ++++++++- nova/db/sqlalchemy/api.py | 27 +++++++++++++++----- nova/tests/unit/db/test_db_api.py | 14 ++++++++--- nova/tests/unit/test_nova_manage.py | 38 ++++++++++++++++++++++++++++- 6 files changed, 93 insertions(+), 16 deletions(-) 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):