Get all the database instances by admin

Currently, there is no way for the admin user to get databases of all
the projects, which is very important for auditing or billing purpose.

With this change, admin user can get all the database instances by
running:

$ openstack database instances list --all-projects

Change-Id: I318907b1d4a63017f4a6f7967312770f8784c1f4
Story: #2005735
Task: #33393
This commit is contained in:
Lingxian Kong 2019-05-20 10:32:34 +12:00
parent d0d53a2c91
commit 7a791177e8
9 changed files with 124 additions and 73 deletions

View File

@ -28,7 +28,7 @@ def append_query_strings(url, **query_strings):
if not query_strings: if not query_strings:
return url return url
query = '&'.join('{0}={1}'.format(key, val) query = '&'.join('{0}={1}'.format(key, val)
for key, val in query_strings.items() if val) for key, val in query_strings.items() if val is not None)
return url + ('?' + query if query else "") return url + ('?' + query if query else "")

View File

@ -69,10 +69,13 @@ def set_attributes_for_print_detail(instance):
class ListDatabaseInstances(command.Lister): class ListDatabaseInstances(command.Lister):
_description = _("List database instances") _description = _("List database instances")
columns = ['ID', 'Name', 'Datastore', 'Datastore Version', 'Status', columns = ['ID', 'Name', 'Datastore', 'Datastore Version', 'Status',
'Flavor ID', 'Size', 'Region'] 'Flavor ID', 'Size', 'Region']
admin_columns = [
'ID', 'Name', 'Tenant ID', 'Datastore', 'Datastore Version', 'Status',
'Flavor ID', 'Size'
]
def get_parser(self, prog_name): def get_parser(self, prog_name):
parser = super(ListDatabaseInstances, self).get_parser(prog_name) parser = super(ListDatabaseInstances, self).get_parser(prog_name)
@ -103,19 +106,34 @@ class ListDatabaseInstances(command.Lister):
"deprecated in the future, retaining just " "deprecated in the future, retaining just "
"--include_clustered.") "--include_clustered.")
) )
parser.add_argument(
'--all-projects',
dest='all_projects',
action="store_true",
default=False,
help=_("Include database instances of all projects (admin only)")
)
return parser return parser
def take_action(self, parsed_args): def take_action(self, parsed_args):
db_instances = self.app.client_manager.database.instances if parsed_args.all_projects:
instances = db_instances.list(limit=parsed_args.limit, db_instances = self.app.client_manager.database.mgmt_instances
marker=parsed_args.marker, cols = self.admin_columns
include_clustered=(parsed_args. else:
include_clustered)) db_instances = self.app.client_manager.database.instances
cols = self.columns
instances = db_instances.list(
limit=parsed_args.limit,
marker=parsed_args.marker,
include_clustered=parsed_args.include_clustered
)
if instances: if instances:
instances = set_attributes_for_print(instances) instances = set_attributes_for_print(instances)
instances = [osc_utils.get_item_properties(i, self.columns) instances = [osc_utils.get_item_properties(i, cols)
for i in instances] for i in instances]
return self.columns, instances
return cols, instances
class ShowDatabaseInstance(command.ShowOne): class ShowDatabaseInstance(command.ShowOne):

View File

@ -158,25 +158,37 @@ class FakeHTTPClient(base_client.HTTPClient):
return r, body return r, body
def get_instances(self, **kw): def get_instances(self, **kw):
return (200, {}, {"instances": [ return (
200,
{},
{ {
"id": "1234", "instances": [
"name": "test-member-1", {
"status": "ACTIVE", "id": "1234",
"ip": ["10.0.0.13"], "name": "test-member-1",
"volume": {"size": 2}, "status": "ACTIVE",
"flavor": {"id": "02"}, "ip": ["10.0.0.13"],
"region": "regionOne", "volume": {"size": 2},
"datastore": {"version": "5.6", "type": "mysql"}}, "flavor": {"id": "02"},
{ "region": "regionOne",
"id": "5678", "datastore": {"version": "5.6", "type": "mysql"},
"name": "test-member-2", "tenant_id": "fake_tenant_id",
"status": "ACTIVE", },
"ip": ["10.0.0.14"], {
"volume": {"size": 2}, "id": "5678",
"flavor": {"id": "2"}, "name": "test-member-2",
"region": "regionOne", "status": "ACTIVE",
"datastore": {"version": "5.6", "type": "mysql"}}]}) "ip": ["10.0.0.14"],
"volume": {"size": 2},
"flavor": {"id": "2"},
"region": "regionOne",
"datastore": {"version": "5.6", "type": "mysql"},
"tenant_id": "fake_tenant_id",
},
]
}
)
def get_instance_counts(self, **kw): def get_instance_counts(self, **kw):
return (200, {}, {"instances": [ return (200, {}, {"instances": [

View File

@ -21,44 +21,71 @@ from troveclient.tests.osc.v1 import fakes
class TestInstances(fakes.TestDatabasev1): class TestInstances(fakes.TestDatabasev1):
fake_instances = fakes.FakeInstances()
def setUp(self): def setUp(self):
super(TestInstances, self).setUp() super(TestInstances, self).setUp()
self.fake_instances = fakes.FakeInstances()
self.instance_client = self.app.client_manager.database.instances self.instance_client = self.app.client_manager.database.instances
self.mgmt_client = self.app.client_manager.database.mgmt_instances
class TestInstanceList(TestInstances): class TestInstanceList(TestInstances):
defaults = { defaults = {
'include_clustered': False, 'include_clustered': False,
'limit': None, 'limit': None,
'marker': None 'marker': None
} }
columns = database_instances.ListDatabaseInstances.columns
values = [('1234', 'test-member-1', 'mysql', '5.6', 'ACTIVE', '02', 2,
'regionOne'), ('5678', 'test-member-2', 'mysql', '5.6',
'ACTIVE', '2', 2, 'regionOne')]
def setUp(self): def setUp(self):
super(TestInstanceList, self).setUp() super(TestInstanceList, self).setUp()
self.cmd = database_instances.ListDatabaseInstances(self.app, None) self.cmd = database_instances.ListDatabaseInstances(self.app, None)
self.data = self.fake_instances.get_instances() self.data = self.fake_instances.get_instances()
self.instance_client.list.return_value = common.Paginated(self.data)
def test_instance_list_defaults(self): def test_instance_list_defaults(self):
self.instance_client.list.return_value = common.Paginated(self.data)
parsed_args = self.check_parser(self.cmd, [], []) parsed_args = self.check_parser(self.cmd, [], [])
columns, data = self.cmd.take_action(parsed_args) columns, data = self.cmd.take_action(parsed_args)
self.instance_client.list.assert_called_once_with(**self.defaults) self.instance_client.list.assert_called_once_with(**self.defaults)
self.assertEqual(self.columns, columns) self.assertEqual(
self.assertEqual(self.values, data) database_instances.ListDatabaseInstances.columns,
columns
)
values = [
('1234', 'test-member-1', 'mysql', '5.6', 'ACTIVE', '02', 2,
'regionOne'),
('5678', 'test-member-2', 'mysql', '5.6', 'ACTIVE', '2', 2,
'regionOne')
]
self.assertEqual(values, data)
def test_instance_list_all_projects(self):
self.mgmt_client.list.return_value = common.Paginated(self.data)
parsed_args = self.check_parser(self.cmd, ["--all-projects"],
[("all_projects", True)])
columns, instances = self.cmd.take_action(parsed_args)
self.mgmt_client.list.assert_called_once_with(**self.defaults)
self.assertEqual(
database_instances.ListDatabaseInstances.admin_columns,
columns
)
expected_instances = [
('1234', 'test-member-1', 'fake_tenant_id', 'mysql', '5.6',
'ACTIVE', '02', 2),
('5678', 'test-member-2', 'fake_tenant_id', 'mysql', '5.6',
'ACTIVE', '2', 2)
]
self.assertEqual(expected_instances, instances)
class TestInstanceShow(TestInstances): class TestInstanceShow(TestInstances):
values = ('mysql', '5.6', '02', '1234', '10.0.0.13', values = ('mysql', '5.6', '02', '1234', '10.0.0.13',
'test-member-1', 'regionOne', 'ACTIVE', 2) 'test-member-1', 'regionOne', 'ACTIVE', 'fake_tenant_id', 2)
def setUp(self): def setUp(self):
super(TestInstanceShow, self).setUp() super(TestInstanceShow, self).setUp()
@ -74,6 +101,7 @@ class TestInstanceShow(TestInstances):
'name', 'name',
'region', 'region',
'status', 'status',
'tenant_id',
'volume', 'volume',
) )

View File

@ -60,6 +60,13 @@ class CommonTest(testtools.TestCase):
self.assertIn("key1=%s" % opts['key1'], result) self.assertIn("key1=%s" % opts['key1'], result)
self.assertNotIn("key2=%s" % opts['key2'], result) self.assertNotIn("key2=%s" % opts['key2'], result)
opts = {'key1': 'value1', 'key2': None, 'key3': False}
result = common.append_query_strings(url, **opts)
self.assertTrue(result.startswith(url + '?'))
self.assertIn("key1=value1", result)
self.assertNotIn("key2", result)
self.assertIn("key3=False", result)
class PaginatedTest(testtools.TestCase): class PaginatedTest(testtools.TestCase):

View File

@ -73,18 +73,17 @@ class ManagementTest(testtools.TestCase):
p, i = self.management.show(1) p, i = self.management.show(1)
self.assertEqual(('/mgmt/instances/instance1', 'instance'), (p, i)) self.assertEqual(('/mgmt/instances/instance1', 'instance'), (p, i))
def test_index(self): def test_list(self):
page_mock = mock.Mock() page_mock = mock.Mock()
self.management._paginated = page_mock self.management._paginated = page_mock
self.management.index(deleted=True)
page_mock.assert_called_with('/mgmt/instances?deleted=true', self.management.list(deleted=True)
'instances', None, None) page_mock.assert_called_with('/mgmt/instances', 'instances', None,
self.management.index(deleted=False) None, query_strings={'deleted': True})
page_mock.assert_called_with('/mgmt/instances?deleted=false',
'instances', None, None) self.management.list(deleted=False, limit=10, marker="foo")
self.management.index(deleted=True, limit=10, marker="foo") page_mock.assert_called_with('/mgmt/instances', 'instances', 10, "foo",
page_mock.assert_called_with('/mgmt/instances?deleted=true', query_strings={"deleted": False})
'instances', 10, "foo")
def test_root_enabled_history(self): def test_root_enabled_history(self):
self.management.api.client.get = mock.Mock(return_value=('resp', None)) self.management.api.client.get = mock.Mock(return_value=('resp', None))

View File

@ -162,7 +162,7 @@ class ShellTest(utils.TestCase):
def test_instance_list(self): def test_instance_list(self):
self.run_command('list') self.run_command('list')
self.assert_called('GET', '/instances') self.assert_called('GET', '/instances?include_clustered=False')
def test_instance_show(self): def test_instance_show(self):
self.run_command('show 1234') self.run_command('show 1234')

View File

@ -23,7 +23,7 @@ from troveclient.v1 import datastores
from troveclient.v1 import flavors from troveclient.v1 import flavors
from troveclient.v1 import instances from troveclient.v1 import instances
from troveclient.v1 import limits from troveclient.v1 import limits
# from troveclient.v1 import management from troveclient.v1 import management
from troveclient.v1 import metadata from troveclient.v1 import metadata
from troveclient.v1 import modules from troveclient.v1 import modules
from troveclient.v1 import quota from troveclient.v1 import quota
@ -82,11 +82,11 @@ class Client(object):
self.configuration_parameters = config_parameters self.configuration_parameters = config_parameters
self.metadata = metadata.Metadata(self) self.metadata = metadata.Metadata(self)
self.modules = modules.Modules(self) self.modules = modules.Modules(self)
self.quota = quota.Quotas(self)
self.mgmt_instances = management.Management(self)
# self.hosts = Hosts(self) # self.hosts = Hosts(self)
self.quota = quota.Quotas(self)
# self.storage = StorageInfo(self) # self.storage = StorageInfo(self)
# self.management = Management(self)
# self.management = MgmtClusters(self) # self.management = MgmtClusters(self)
# self.mgmt_flavor = MgmtFlavors(self) # self.mgmt_flavor = MgmtFlavors(self)
# self.accounts = Accounts(self) # self.accounts = Accounts(self)

View File

@ -35,10 +35,6 @@ class Management(base.ManagerWithFind):
"""Manage :class:`Instances` resources.""" """Manage :class:`Instances` resources."""
resource_class = instances.Instance resource_class = instances.Instance
# Appease the abc gods
def list(self):
pass
def show(self, instance): def show(self, instance):
"""Get details of one instance. """Get details of one instance.
@ -48,22 +44,13 @@ class Management(base.ManagerWithFind):
return self._get("/mgmt/instances/%s" % base.getid(instance), return self._get("/mgmt/instances/%s" % base.getid(instance),
'instance') 'instance')
def index(self, deleted=None, limit=None, marker=None): def list(self, limit=None, marker=None, deleted=False, **kwargs):
"""Show an overview of all local instances. """Get all the database instances."""
url = "/mgmt/instances"
kwargs["deleted"] = deleted
Optionally, filter by deleted status. return self._paginated(url, "instances", limit, marker,
query_strings=kwargs)
:rtype: list of :class:`Instance`.
"""
form = ''
if deleted is not None:
if deleted:
form = "?deleted=true"
else:
form = "?deleted=false"
url = "/mgmt/instances%s" % form
return self._paginated(url, "instances", limit, marker)
def root_enabled_history(self, instance): def root_enabled_history(self, instance):
"""Get root access history of one instance.""" """Get root access history of one instance."""