diff --git a/nova/cmd/manage.py b/nova/cmd/manage.py index b1b23933ab91..326acba81de0 100644 --- a/nova/cmd/manage.py +++ b/nova/cmd/manage.py @@ -56,6 +56,7 @@ from __future__ import print_function import functools import os +import re import sys import traceback @@ -119,6 +120,16 @@ def param2id(object_id): return object_id +def mask_passwd_in_url(url): + parsed = urlparse.urlparse(url) + safe_netloc = re.sub(':.*@', ':****@', parsed.netloc) + new_parsed = urlparse.ParseResult( + parsed.scheme, safe_netloc, + parsed.path, parsed.params, + parsed.query, parsed.fragment) + return urlparse.urlunparse(new_parsed) + + class ShellCommands(object): # TODO(stephenfin): Remove this during the Queens cycle @@ -1493,7 +1504,7 @@ class CellV2Commands(object): return 0 @args('--verbose', action='store_true', - help=_('Show more details than just the cell name and uuid.')) + help=_('Show sensitive details, such as passwords')) def list_cells(self, verbose=False): """Lists the v2 cells in the deployment. @@ -1503,15 +1514,18 @@ class CellV2Commands(object): cell_mappings = objects.CellMappingList.get_all( context.get_admin_context()) - field_names = [_('Name'), _('UUID')] - if verbose: - field_names.extend([_('Transport URL'), _('Database Connection')]) + field_names = [_('Name'), _('UUID'), _('Transport URL'), + _('Database Connection')] t = prettytable.PrettyTable(field_names) for cell in sorted(cell_mappings, key=lambda _cell: _cell.name): fields = [cell.name, cell.uuid] if verbose: fields.extend([cell.transport_url, cell.database_connection]) + else: + fields.extend([ + mask_passwd_in_url(cell.transport_url), + mask_passwd_in_url(cell.database_connection)]) t.add_row(fields) print(t) return 0 diff --git a/nova/tests/unit/test_nova_manage.py b/nova/tests/unit/test_nova_manage.py index f139cb886955..65165db1db56 100644 --- a/nova/tests/unit/test_nova_manage.py +++ b/nova/tests/unit/test_nova_manage.py @@ -39,6 +39,42 @@ from nova.tests import uuidsentinel CONF = conf.CONF +class UtilitiesTestCase(test.NoDBTestCase): + + def test_mask_passwd(self): + # try to trip up the regex match with extra : and @. + url1 = ("http://user:pass@domain.com:1234/something?" + "email=me@somewhere.com") + self.assertEqual( + ("http://user:****@domain.com:1234/something?" + "email=me@somewhere.com"), + manage.mask_passwd_in_url(url1)) + + # pretty standard kinds of urls that we expect, have different + # schemes. This ensures none of the parts get lost. + url2 = "mysql+pymysql://root:pass@127.0.0.1/nova_api?charset=utf8" + self.assertEqual( + "mysql+pymysql://root:****@127.0.0.1/nova_api?charset=utf8", + manage.mask_passwd_in_url(url2)) + + url3 = "rabbit://stackrabbit:pass@10.42.0.53:5672/" + self.assertEqual( + "rabbit://stackrabbit:****@10.42.0.53:5672/", + manage.mask_passwd_in_url(url3)) + + url4 = ("mysql+pymysql://nova:my_password@my_IP/nova_api?" + "charset=utf8&ssl_ca=/etc/nova/tls/mysql/ca-cert.pem" + "&ssl_cert=/etc/nova/tls/mysql/server-cert.pem" + "&ssl_key=/etc/nova/tls/mysql/server-key.pem") + url4_safe = ("mysql+pymysql://nova:****@my_IP/nova_api?" + "charset=utf8&ssl_ca=/etc/nova/tls/mysql/ca-cert.pem" + "&ssl_cert=/etc/nova/tls/mysql/server-cert.pem" + "&ssl_key=/etc/nova/tls/mysql/server-key.pem") + self.assertEqual( + url4_safe, + manage.mask_passwd_in_url(url4)) + + class FloatingIpCommandsTestCase(test.NoDBTestCase): def setUp(self): super(FloatingIpCommandsTestCase, self).setUp() @@ -1454,13 +1490,30 @@ class CellV2CommandsTestCase(test.NoDBTestCase): self.assertIn('--database_connection', self.output.getvalue()) def test_list_cells_no_cells_verbose_false(self): + ctxt = context.RequestContext() + # This uses fake uuids so the table can stay under 80 characeters. + cell_mapping0 = objects.CellMapping( + context=ctxt, uuid='00000000-0000-0000', + database_connection='fake://user1:pass1@host1/db0', + transport_url='none://user1:pass1@host1/', + name='cell0') + cell_mapping0.create() + cell_mapping1 = objects.CellMapping( + context=ctxt, uuid='9e36a3ed-3eb6-4327', + database_connection='fake://user1@host1/db0', + transport_url='none://user1@host1/vhost1', + name='cell1') + cell_mapping1.create() self.assertEqual(0, self.commands.list_cells()) output = self.output.getvalue().strip() self.assertEqual('''\ -+------+------+ -| Name | UUID | -+------+------+ -+------+------+''', output) ++-------+--------------------+---------------------------+-----------------------------+ +| Name | UUID | Transport URL | Database Connection | ++-------+--------------------+---------------------------+-----------------------------+ +| cell0 | 00000000-0000-0000 | none://user1:****@host1/ | fake://user1:****@host1/db0 | +| cell1 | 9e36a3ed-3eb6-4327 | none://user1@host1/vhost1 | fake://user1@host1/db0 | ++-------+--------------------+---------------------------+-----------------------------+''', # noqa + output) def test_list_cells_multiple_sorted_verbose_true(self): ctxt = context.RequestContext()