diff --git a/releasenotes/notes/logging-support-f999a1b1b342eb4d.yaml b/releasenotes/notes/logging-support-f999a1b1b342eb4d.yaml new file mode 100644 index 0000000..ebe8b37 --- /dev/null +++ b/releasenotes/notes/logging-support-f999a1b1b342eb4d.yaml @@ -0,0 +1,5 @@ +--- +features: + - Added support for logging features in the dashboard. + This includes listing logs that can be retrieved and + viewing, publishing and stop collection of log contents. diff --git a/trove_dashboard/api/trove.py b/trove_dashboard/api/trove.py index 9be2bb3..6c7c9df 100644 --- a/trove_dashboard/api/trove.py +++ b/trove_dashboard/api/trove.py @@ -297,3 +297,31 @@ def datastore_list(request): def datastore_version_list(request, datastore): return troveclient(request).datastore_versions.list(datastore) + + +def log_list(request, instance_id): + return troveclient(request).instances.log_list(instance_id) + + +def log_enable(request, instance_id, log_name): + return troveclient(request).instances.log_enable(instance_id, log_name) + + +def log_disable(request, instance_id, log_name): + return troveclient(request).instances.log_disable(instance_id, log_name) + + +def log_publish(request, instance_id, log_name): + return troveclient(request).instances.log_publish(instance_id, log_name) + + +def log_discard(request, instance_id, log_name): + return troveclient(request).instances.log_discard(instance_id, log_name) + + +def log_tail(request, instance_id, log_name, publish, lines, swift=None): + return troveclient(request).instances.log_generator(instance_id, + log_name, + publish=publish, + lines=lines, + swift=swift) diff --git a/trove_dashboard/content/databases/logs/__init__.py b/trove_dashboard/content/databases/logs/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/trove_dashboard/content/databases/logs/tables.py b/trove_dashboard/content/databases/logs/tables.py new file mode 100644 index 0000000..7decdd0 --- /dev/null +++ b/trove_dashboard/content/databases/logs/tables.py @@ -0,0 +1,156 @@ +# Copyright 2016 Tesora Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from django.core import urlresolvers +from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import ungettext_lazy + +from horizon import tables + +from trove_dashboard import api + + +class PublishLog(tables.BatchAction): + @staticmethod + def action_present(count): + return ungettext_lazy( + u"Publish Log", + u"Publish Logs", + count + ) + + @staticmethod + def action_past(count): + return ungettext_lazy( + u"Published Log", + u"Published Logs", + count + ) + + name = "publish_log" + + def action(self, request, obj_id): + instance_id = self.table.kwargs['instance_id'] + api.trove.log_publish(request, instance_id, obj_id) + + +class DiscardLog(tables.BatchAction): + @staticmethod + def action_present(count): + return ungettext_lazy( + u"Discard Log", + u"Discard Logs", + count + ) + + @staticmethod + def action_past(count): + return ungettext_lazy( + u"Discarded Log", + u"Discarded Logs", + count + ) + + name = "discard_log" + + def action(self, request, obj_id): + instance_id = self.table.kwargs['instance_id'] + api.trove.log_discard(request, instance_id, obj_id) + + +class EnableLog(tables.BatchAction): + @staticmethod + def action_present(count): + return ungettext_lazy( + u"Enable Log", + u"Enable Logs", + count + ) + + @staticmethod + def action_past(count): + return ungettext_lazy( + u"Enabled Log", + u"Enabled Logs", + count + ) + + name = "enable_log" + + def action(self, request, obj_id): + instance_id = self.table.kwargs['instance_id'] + api.trove.log_enable(request, instance_id, obj_id) + + +class DisableLog(tables.BatchAction): + @staticmethod + def action_present(count): + return ungettext_lazy( + u"Disable Log", + u"Disable Logs", + count + ) + + @staticmethod + def action_past(count): + return ungettext_lazy( + u"Disabled Log", + u"Disabled Logs", + count + ) + + name = "disable_log" + + def action(self, request, obj_id): + instance_id = self.table.kwargs['instance_id'] + api.trove.log_disable(request, instance_id, obj_id) + + def allowed(self, request, datum=None): + if datum: + return datum.type != "SYS" + return False + + +class ViewLog(tables.LinkAction): + name = "view_log" + verbose_name = _("View Log") + url = "horizon:project:databases:logs:log_contents" + + def get_link_url(self, datum): + instance_id = self.table.kwargs['instance_id'] + return urlresolvers.reverse(self.url, + args=(instance_id, + datum.name)) + + def allowed(self, request, datum=None): + if datum: + return datum.published > 0 + return False + + +class LogsTable(tables.DataTable): + name = tables.Column('name', verbose_name=_('Name')) + type = tables.Column('type', verbose_name=_("Type")) + status = tables.Column('status', verbose_name=_("Status")) + published = tables.Column('published', verbose_name=_('Published (bytes)')) + pending = tables.Column('pending', verbose_name=_('Publishable (bytes)')) + container = tables.Column('container', verbose_name=_('Container')) + + class Meta(object): + name = "logs" + verbose_name = _("Logs") + row_actions = (ViewLog, PublishLog, EnableLog, DisableLog, DiscardLog) + + def get_object_id(self, datum): + return datum.name diff --git a/trove_dashboard/content/databases/logs/tests.py b/trove_dashboard/content/databases/logs/tests.py new file mode 100644 index 0000000..cb8a03d --- /dev/null +++ b/trove_dashboard/content/databases/logs/tests.py @@ -0,0 +1,361 @@ +# Copyright 2016 Tesora Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import logging + +from django.core.urlresolvers import reverse +from django import http + +from mox3 import mox +from mox3.mox import IsA # noqa +import six + +from trove_dashboard import api +from trove_dashboard.test import helpers as test + +from swiftclient import client as swift_client + +LINES = 50 + + +class LogsTests(test.TestCase): + def stub_swiftclient(self, expected_calls=1): + if not hasattr(self, "swiftclient"): + self.mox.StubOutWithMock(swift_client, 'Connection') + self.swiftclient = self.mox.CreateMock(swift_client.Connection) + while expected_calls: + (swift_client.Connection(None, + mox.IgnoreArg(), + None, + preauthtoken=mox.IgnoreArg(), + preauthurl=mox.IgnoreArg(), + cacert=None, + insecure=False, + auth_version="2.0") + .AndReturn(self.swiftclient)) + expected_calls -= 1 + return self.swiftclient + + @test.create_stubs({ + api.trove: ('flavor_get', 'instance_get', 'log_list', 'root_show')}) + def test_log_tab(self): + database = self.databases.first() + database_id = database.id + + (api.trove.instance_get(IsA(http.HttpRequest), IsA(six.text_type)) + .AndReturn(database)) + (api.trove.log_list(IsA(http.HttpRequest), database_id) + .AndReturn(self.logs.list())) + (api.trove.flavor_get(IsA(http.HttpRequest), database.flavor["id"]) + .AndReturn(self.flavors.first())) + (api.trove.root_show(IsA(http.HttpRequest), database.id) + .AndReturn(self.database_user_roots.first())) + + self.mox.ReplayAll() + + detail_url = reverse('horizon:project:databases:detail', + args=[database_id]) + url = detail_url + '?tab=instance_details__logs_tab' + res = self.client.get(url) + table_data = res.context['logs_table'].data + self.assertItemsEqual(self.logs.list(), table_data) + self.assertTemplateUsed( + res, 'horizon/common/_detail_table.html') + + @test.create_stubs({ + api.trove: ('flavor_get', 'instance_get', 'log_list', 'root_show')}) + def test_log_tab_exception(self): + database = self.databases.first() + database_id = database.id + + (api.trove.instance_get(IsA(http.HttpRequest), IsA(six.text_type)) + .AndReturn(database)) + (api.trove.log_list(IsA(http.HttpRequest), database_id) + .AndRaise(self.exceptions.trove)) + (api.trove.flavor_get(IsA(http.HttpRequest), database.flavor["id"]) + .AndReturn(self.flavors.first())) + (api.trove.root_show(IsA(http.HttpRequest), database.id) + .AndReturn(self.database_user_roots.first())) + + self.mox.ReplayAll() + + detail_url = reverse('horizon:project:databases:detail', + args=[database_id]) + url = detail_url + '?tab=instance_details__logs_tab' + + toSuppress = ["trove_dashboard.content.databases.tabs"] + + loggers = [] + for cls in toSuppress: + logger = logging.getLogger(cls) + loggers.append((logger, logger.getEffectiveLevel())) + logger.setLevel(logging.CRITICAL) + + try: + res = self.client.get(url) + table_data = res.context['logs_table'].data + self.assertNotEqual(len(self.logs.list()), len(table_data)) + self.assertTemplateUsed( + res, 'horizon/common/_detail_table.html') + finally: + # Restore the previous log levels + for (log, level) in loggers: + log.setLevel(level) + + @test.create_stubs({ + api.trove: ('flavor_get', 'instance_get', 'log_list', 'log_publish',) + }) + def test_log_publish(self): + database = self.databases.first() + database_id = database.id + log = self.logs.first() + (api.trove.instance_get(IsA(http.HttpRequest), IsA(six.text_type)) + .AndReturn(database)) + (api.trove.log_list(IsA(http.HttpRequest), database_id) + .AndReturn(self.logs.list())) + (api.trove.flavor_get(IsA(http.HttpRequest), database.flavor["id"]) + .AndReturn(self.flavors.first())) + (api.trove.log_publish(IsA(http.HttpRequest), database_id, log.name) + .AndReturn(None)) + + self.mox.ReplayAll() + + detail_url = reverse('horizon:project:databases:detail', + args=[database_id]) + url = detail_url + '?tab=instance_details__logs_tab' + action_string = u"logs__%s_log__%s" % ('publish', log.name) + form_data = {'action': action_string} + res = self.client.post(url, form_data) + self.assertRedirectsNoFollow(res, url) + + @test.create_stubs({ + api.trove: ('flavor_get', 'instance_get', 'log_list', 'log_publish',) + }) + def test_log_publish_exception(self): + database = self.databases.first() + database_id = database.id + log = self.logs.first() + (api.trove.instance_get(IsA(http.HttpRequest), IsA(six.text_type)) + .AndReturn(database)) + (api.trove.log_list(IsA(http.HttpRequest), database_id) + .AndReturn(self.logs.list())) + (api.trove.flavor_get(IsA(http.HttpRequest), database.flavor["id"]) + .AndReturn(self.flavors.first())) + (api.trove.log_publish(IsA(http.HttpRequest), database_id, log.name) + .AndRaise(self.exceptions.trove)) + + self.mox.ReplayAll() + + detail_url = reverse('horizon:project:databases:detail', + args=[database_id]) + url = detail_url + '?tab=instance_details__logs_tab' + action_string = u"logs__%s_log__%s" % ('publish', log.name) + form_data = {'action': action_string} + res = self.client.post(url, form_data) + self.assertRedirectsNoFollow(res, url) + + @test.create_stubs({ + api.trove: ('flavor_get', 'instance_get', 'log_list', 'log_enable',) + }) + def test_log_enable(self): + database = self.databases.first() + database_id = database.id + log = self.logs.first() + (api.trove.instance_get(IsA(http.HttpRequest), IsA(six.text_type)) + .AndReturn(database)) + (api.trove.log_list(IsA(http.HttpRequest), database_id) + .AndReturn(self.logs.list())) + (api.trove.flavor_get(IsA(http.HttpRequest), database.flavor["id"]) + .AndReturn(self.flavors.first())) + (api.trove.log_enable(IsA(http.HttpRequest), database_id, log.name) + .AndReturn(None)) + + self.mox.ReplayAll() + + detail_url = reverse('horizon:project:databases:detail', + args=[database_id]) + url = detail_url + '?tab=instance_details__logs_tab' + action_string = u"logs__%s_log__%s" % ('enable', log.name) + form_data = {'action': action_string} + res = self.client.post(url, form_data) + self.assertRedirectsNoFollow(res, url) + + @test.create_stubs({ + api.trove: ('flavor_get', 'instance_get', 'log_list', 'log_enable',) + }) + def test_log_enable_exception(self): + database = self.databases.first() + database_id = database.id + log = self.logs.first() + (api.trove.instance_get(IsA(http.HttpRequest), IsA(six.text_type)) + .AndReturn(database)) + (api.trove.log_list(IsA(http.HttpRequest), database_id) + .AndReturn(self.logs.list())) + (api.trove.flavor_get(IsA(http.HttpRequest), database.flavor["id"]) + .AndReturn(self.flavors.first())) + (api.trove.log_enable(IsA(http.HttpRequest), database_id, log.name) + .AndRaise(self.exceptions.trove)) + + self.mox.ReplayAll() + + detail_url = reverse('horizon:project:databases:detail', + args=[database_id]) + url = detail_url + '?tab=instance_details__logs_tab' + action_string = u"logs__%s_log__%s" % ('enable', log.name) + form_data = {'action': action_string} + res = self.client.post(url, form_data) + self.assertRedirectsNoFollow(res, url) + + @test.create_stubs({ + api.trove: ('flavor_get', 'instance_get', 'log_list', 'log_discard',) + }) + def test_log_discard(self): + database = self.databases.first() + database_id = database.id + log = self.logs.first() + (api.trove.instance_get(IsA(http.HttpRequest), IsA(six.text_type)) + .AndReturn(database)) + (api.trove.log_list(IsA(http.HttpRequest), database_id) + .AndReturn(self.logs.list())) + (api.trove.flavor_get(IsA(http.HttpRequest), database.flavor["id"]) + .AndReturn(self.flavors.first())) + (api.trove.log_discard(IsA(http.HttpRequest), database_id, log.name) + .AndReturn(None)) + + self.mox.ReplayAll() + + detail_url = reverse('horizon:project:databases:detail', + args=[database_id]) + url = detail_url + '?tab=instance_details__logs_tab' + action_string = u"logs__%s_log__%s" % ('discard', log.name) + form_data = {'action': action_string} + res = self.client.post(url, form_data) + self.assertRedirectsNoFollow(res, url) + + @test.create_stubs({ + api.trove: ('flavor_get', 'instance_get', 'log_list', 'log_discard',) + }) + def test_log_discard_exception(self): + database = self.databases.first() + database_id = database.id + log = self.logs.first() + (api.trove.instance_get(IsA(http.HttpRequest), IsA(six.text_type)) + .AndReturn(database)) + (api.trove.log_list(IsA(http.HttpRequest), database_id) + .AndReturn(self.logs.list())) + (api.trove.flavor_get(IsA(http.HttpRequest), database.flavor["id"]) + .AndReturn(self.flavors.first())) + (api.trove.log_discard(IsA(http.HttpRequest), database_id, log.name) + .AndRaise(self.exceptions.trove)) + + self.mox.ReplayAll() + + detail_url = reverse('horizon:project:databases:detail', + args=[database_id]) + url = detail_url + '?tab=instance_details__logs_tab' + action_string = u"logs__%s_log__%s" % ('discard', log.name) + form_data = {'action': action_string} + res = self.client.post(url, form_data) + self.assertRedirectsNoFollow(res, url) + + @test.create_stubs({ + api.trove: ('flavor_get', 'instance_get', 'log_list', 'log_disable',) + }) + def test_log_disable(self): + database = self.databases.first() + database_id = database.id + log = self.logs.list()[3] + (api.trove.instance_get(IsA(http.HttpRequest), IsA(six.text_type)) + .AndReturn(database)) + (api.trove.log_list(IsA(http.HttpRequest), database_id) + .AndReturn(self.logs.list())) + (api.trove.flavor_get(IsA(http.HttpRequest), database.flavor["id"]) + .AndReturn(self.flavors.first())) + (api.trove.log_disable(IsA(http.HttpRequest), database_id, log.name) + .AndReturn(None)) + + self.mox.ReplayAll() + + detail_url = reverse('horizon:project:databases:detail', + args=[database_id]) + url = detail_url + '?tab=instance_details__logs_tab' + action_string = u"logs__%s_log__%s" % ('disable', log.name) + form_data = {'action': action_string} + res = self.client.post(url, form_data) + self.assertRedirectsNoFollow(res, url) + + @test.create_stubs({ + api.trove: ('flavor_get', 'instance_get', 'log_list', 'log_disable',) + }) + def test_log_disable_exception(self): + database = self.databases.first() + database_id = database.id + log = self.logs.list()[3] + (api.trove.instance_get(IsA(http.HttpRequest), IsA(six.text_type)) + .AndReturn(database)) + (api.trove.log_list(IsA(http.HttpRequest), database_id) + .AndReturn(self.logs.list())) + (api.trove.flavor_get(IsA(http.HttpRequest), database.flavor["id"]) + .AndReturn(self.flavors.first())) + (api.trove.log_disable(IsA(http.HttpRequest), database_id, log.name) + .AndRaise(self.exceptions.trove)) + + self.mox.ReplayAll() + + detail_url = reverse('horizon:project:databases:detail', + args=[database_id]) + url = detail_url + '?tab=instance_details__logs_tab' + action_string = u"logs__%s_log__%s" % ('disable', log.name) + form_data = {'action': action_string} + res = self.client.post(url, form_data) + self.assertRedirectsNoFollow(res, url) + + @test.create_stubs({api.trove: ('log_tail',)}) + def test_view_log(self): + CONSOLE_OUTPUT = 'superspecialuniquetext' + (api.trove.log_tail(IsA(http.HttpRequest), + IsA(six.string_types), + 'guest.log', + False, + LINES, + self.stub_swiftclient()) + .AndReturn(lambda: [CONSOLE_OUTPUT])) + + self.mox.ReplayAll() + + url = reverse('horizon:project:databases:logs:log_contents', + args=('id', 'guest.log')) + res = self.client.get(url) + + self.assertNoMessages() + self.assertIsInstance(res, http.HttpResponse) + self.assertContains(res, CONSOLE_OUTPUT) + + @test.create_stubs({api.trove: ('log_tail',)}) + def test_view_log_exception(self): + (api.trove.log_tail(IsA(http.HttpRequest), + IsA(six.string_types), + 'guest.log', + False, + LINES, + self.stub_swiftclient()) + .AndRaise(self.exceptions.trove)) + + self.mox.ReplayAll() + + url = reverse('horizon:project:databases:logs:log_contents', + args=('id', 'guest.log')) + res = self.client.get(url) + + self.assertContains(res, "Unable to load") diff --git a/trove_dashboard/content/databases/logs/urls.py b/trove_dashboard/content/databases/logs/urls.py new file mode 100644 index 0000000..ae734e8 --- /dev/null +++ b/trove_dashboard/content/databases/logs/urls.py @@ -0,0 +1,32 @@ +# Copyright 2016 Tesora Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from django.conf.urls import patterns +from django.conf.urls import url + +from trove_dashboard.content.databases.logs import views + + +VIEWS_MOD = ('trove_dashboard.content.databases.logs.views') + +LOGS = r'^(?P[^/]+)/%s$' + +urlpatterns = patterns( + VIEWS_MOD, + url(LOGS % 'console', 'console', name='console'), + url(LOGS % 'download_log', 'download_log', name='download_log'), + url(LOGS % 'full_log', 'full_log', name='full_log'), + url(LOGS % 'log_contents', + views.LogContentsView.as_view(), name='log_contents'), +) diff --git a/trove_dashboard/content/databases/logs/views.py b/trove_dashboard/content/databases/logs/views.py new file mode 100644 index 0000000..c1aec92 --- /dev/null +++ b/trove_dashboard/content/databases/logs/views.py @@ -0,0 +1,129 @@ +# Copyright 2016 Tesora Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from django import http +from django import shortcuts +from django.utils.translation import ugettext_lazy as _ +from django.views import generic + +from horizon import exceptions +from horizon import messages + +from openstack_dashboard import api as dash_api +from trove_dashboard import api + +FULL_LOG_VALUE = 0 +DEFAULT_LINES = 50 + + +class LogContentsView(generic.TemplateView): + template_name = 'project/databases/logs/view_log.html' + preload = False + + def get_context_data(self, **kwargs): + context = super(LogContentsView, self).get_context_data(**kwargs) + context["instance_id"] = kwargs['instance_id'] + context["filename"] = kwargs['filename'] + context["publish"] = '' + + try: + log_length = int(kwargs['lines']) + except Exception: + log_length = DEFAULT_LINES + + context["log_length"] = log_length + context["log_contents"] = get_contents(self.request, + kwargs['instance_id'], + kwargs['filename'], + False, + log_length) + return context + + +def get_contents(request, instance_id, filename, publish, lines): + try: + log_generator = api.trove.log_tail(request, + instance_id, + filename, + publish, + lines, + dash_api.swift.swift_api(request)) + data = "" + for log_part in log_generator(): + data += log_part + except Exception as e: + data = _('Unable to load {0} log\n{1}').format(filename, e.message) + return data + + +def build_response(request, instance_id, filename, tail): + data = (_('Unable to load {0} log for instance "{1}".') + .format(filename, instance_id)) + + if request.GET.get('publish'): + publish = True + else: + publish = False + + try: + data = get_contents(request, + instance_id, + filename, + publish, + int(tail)) + except Exception: + exceptions.handle(request, ignore=True) + return http.HttpResponse(data.encode('utf-8'), content_type='text/plain') + + +def console(request, instance_id, filename): + tail = request.GET.get('length') + if not tail or (tail and not tail.isdigit()): + msg = _('Log length must be a nonnegative integer.') + messages.warning(request, msg) + data = (_('Unable to load {0} log for instance "{1}".') + .format(filename, instance_id)) + return http.HttpResponse(data.encode('utf-8'), + content_type='text/plain') + + return build_response(request, instance_id, filename, tail) + + +def full_log(request, instance_id, filename): + return build_response(request, instance_id, filename, FULL_LOG_VALUE) + + +def download_log(request, instance_id, filename): + try: + publish_value = request.GET.get('publish') + if publish_value: + publish = True + else: + publish = False + + data = get_contents(request, + instance_id, + filename, + publish, + FULL_LOG_VALUE) + response = http.HttpResponse() + response.write(data) + response['Content-Disposition'] = ('attachment; ' + 'filename="%s.log"' % filename) + response['Content-Length'] = str(len(response.content)) + return response + + except Exception as e: + messages.error(request, _('Error downloading log file: %s') % e) + return shortcuts.redirect(request.build_absolute_uri()) diff --git a/trove_dashboard/content/databases/tabs.py b/trove_dashboard/content/databases/tabs.py index 6e7e30a..95ad748 100644 --- a/trove_dashboard/content/databases/tabs.py +++ b/trove_dashboard/content/databases/tabs.py @@ -12,6 +12,8 @@ # License for the specific language governing permissions and limitations # under the License. +import logging + from django import template from django.utils.translation import ugettext_lazy as _ @@ -19,9 +21,13 @@ from horizon import exceptions from horizon import tabs from trove_dashboard import api from trove_dashboard.content.databases import db_capability +from trove_dashboard.content.databases.logs import tables as log_tables from trove_dashboard.content.databases import tables +LOG = logging.getLogger(__name__) + + class OverviewTab(tabs.Tab): name = _("Overview") slug = "overview" @@ -136,7 +142,26 @@ class BackupsTab(tabs.TableTab): return request.user.has_perm('openstack.services.object-store') +class LogsTab(tabs.TableTab): + table_classes = [log_tables.LogsTable] + name = _("Logs") + slug = "logs_tab" + template_name = "horizon/common/_detail_table.html" + preload = False + + def get_logs_data(self): + instance = self.tab_group.kwargs['instance'] + try: + logs = api.trove.log_list(self.request, instance.id) + return logs + except Exception as e: + LOG.exception( + _('Unable to retrieve list of logs.\n%s') % e.message) + logs = [] + return logs + + class InstanceDetailTabs(tabs.TabGroup): slug = "instance_details" - tabs = (OverviewTab, UserTab, DatabaseTab, BackupsTab) + tabs = (OverviewTab, UserTab, DatabaseTab, BackupsTab, LogsTab) sticky = True diff --git a/trove_dashboard/content/databases/templates/databases/logs/_detail_log.html b/trove_dashboard/content/databases/templates/databases/logs/_detail_log.html new file mode 100644 index 0000000..3d39238 --- /dev/null +++ b/trove_dashboard/content/databases/templates/databases/logs/_detail_log.html @@ -0,0 +1,22 @@ +{% load i18n %} +{% load url from future %} + +
+
+
+ + + + + + {% trans "Return to Log List" %} + {% trans "Download" %} + {% trans "View Full Log" %} +
+
+
+ +
+    {{ log_contents }}
+  
+
diff --git a/trove_dashboard/content/databases/templates/databases/logs/_log_contents.html b/trove_dashboard/content/databases/templates/databases/logs/_log_contents.html new file mode 100644 index 0000000..9758b53 --- /dev/null +++ b/trove_dashboard/content/databases/templates/databases/logs/_log_contents.html @@ -0,0 +1,15 @@ +{% extends "horizon/common/_modal.html" %} +{% load i18n %} +{% load url from future %} + +{% block modal-header %}{% trans "Log Contents" %}{% endblock %} + +{% block modal-body %} +
+  {{ log.data }}
+
+{% endblock %} + +{% block modal-footer %} + {% trans "Close" %} +{% endblock %} diff --git a/trove_dashboard/content/databases/templates/databases/logs/log_contents.html b/trove_dashboard/content/databases/templates/databases/logs/log_contents.html new file mode 100644 index 0000000..91d8f74 --- /dev/null +++ b/trove_dashboard/content/databases/templates/databases/logs/log_contents.html @@ -0,0 +1,11 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Log Contents" %}{% endblock %} + +{% block main %} +
+
+ {% include "project/databases/logs/_log_contents.html" %} +
+
+{% endblock %} \ No newline at end of file diff --git a/trove_dashboard/content/databases/templates/databases/logs/view_log.html b/trove_dashboard/content/databases/templates/databases/logs/view_log.html new file mode 100644 index 0000000..9036c57 --- /dev/null +++ b/trove_dashboard/content/databases/templates/databases/logs/view_log.html @@ -0,0 +1,11 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Log" %}{% endblock %} + +{% block page_header %} + {% include "horizon/common/_page_header.html" with title=_("Log: ")|add:filename %} +{% endblock page_header %} + +{% block main %} + {% include "project/databases/logs/_detail_log.html" %} +{% endblock %} diff --git a/trove_dashboard/content/databases/urls.py b/trove_dashboard/content/databases/urls.py index 46d86ee..a1b54a3 100644 --- a/trove_dashboard/content/databases/urls.py +++ b/trove_dashboard/content/databases/urls.py @@ -12,13 +12,15 @@ # License for the specific language governing permissions and limitations # under the License. +from django.conf.urls import include from django.conf.urls import patterns from django.conf.urls import url +from trove_dashboard.content.databases.logs import urls as logs_urls from trove_dashboard.content.databases import views - -INSTANCES = r'^(?P[^/]+)/%s$' +BASEINSTANCES = r'^(?P[^/]+)/%s' +INSTANCES = BASEINSTANCES + '$' USERS = r'^(?P[^/]+)/(?P[^/]+)/%s$' @@ -44,4 +46,5 @@ urlpatterns = patterns( name='promote_to_replica_source'), url(INSTANCES % 'manage_root', views.ManageRootView.as_view(), name='manage_root'), + url(BASEINSTANCES % 'logs/', include(logs_urls, namespace='logs')), ) diff --git a/trove_dashboard/test/test_data/trove_data.py b/trove_dashboard/test/test_data/trove_data.py index 1ad3a63..2992342 100644 --- a/trove_dashboard/test/test_data/trove_data.py +++ b/trove_dashboard/test/test_data/trove_data.py @@ -367,6 +367,42 @@ VERSION_VERTICA_7_1 = { "id": "600a6d52-8347-4e00-8e4c-f4fa9cf96af1" } +LOG_1 = { + "name": "guest", + "type": "SYS", + "status": "Partial", + "published": 5000, + "pending": 100, + "container": "guest_container" +} + +LOG_2 = { + "name": "slow_query", + "type": "USER", + "status": "Disabled", + "published": 0, + "pending": 0, + "container": "None" +} + +LOG_3 = { + "name": "error", + "type": "SYS", + "status": "Unavailable", + "published": 0, + "pending": 0, + "container": "None" +} + +LOG_4 = { + "name": "general", + "type": "USER", + "status": "Ready", + "published": 0, + "pending": 1000, + "container": "None" +} + def data(TEST): cluster1 = clusters.Cluster(clusters.Clusters(None), @@ -413,6 +449,11 @@ def data(TEST): DatastoreVersion(datastores.DatastoreVersions(None), VERSION_VERTICA_7_1) + log1 = instances.DatastoreLog(instances.Instances(None), LOG_1) + log2 = instances.DatastoreLog(instances.Instances(None), LOG_2) + log3 = instances.DatastoreLog(instances.Instances(None), LOG_3) + log4 = instances.DatastoreLog(instances.Instances(None), LOG_4) + TEST.trove_clusters = utils.TestDataContainer() TEST.trove_clusters.add(cluster1) TEST.trove_clusters.add(cluster2) @@ -443,3 +484,6 @@ def data(TEST): TEST.datastore_versions.add(version_redis_3_0) TEST.datastore_versions.add(version_mongodb_2_6) TEST.datastore_versions.add(version1) + + TEST.logs = utils.TestDataContainer() + TEST.logs.add(log1, log2, log3, log4)