From 55bd481865a5c26127adb5c222a726aef41c6f6f Mon Sep 17 00:00:00 2001 From: Duk Loi Date: Tue, 23 Feb 2016 16:26:45 -0500 Subject: [PATCH] Trove add support for log retrieval Add new log tab table to instance details to display the list of logs that can be retrieved. The table has row options to view the log in a separate panel, publish new log data and stop collection of log data. The log data is displayed in a new panel. In this panel the number of lines to display can be changed. Also checking the publish checkbox and refreshing the log data will publish any new log data. There are three buttons that perform the following actions: * view the entire log in a separate window * download the entire log * return to the log list panel The log code is placed in a subdirectory of database to keep the code more manageable. Added the supporting log commands to the api. Change-Id: I577bcb598932d4a4568e22176671f1b67aceb787 Closes-Bug: #1548514 --- .../logging-support-f999a1b1b342eb4d.yaml | 5 + trove_dashboard/api/trove.py | 28 ++ .../content/databases/logs/__init__.py | 0 .../content/databases/logs/tables.py | 156 ++++++++ .../content/databases/logs/tests.py | 361 ++++++++++++++++++ .../content/databases/logs/urls.py | 32 ++ .../content/databases/logs/views.py | 129 +++++++ trove_dashboard/content/databases/tabs.py | 27 +- .../templates/databases/logs/_detail_log.html | 22 ++ .../databases/logs/_log_contents.html | 15 + .../databases/logs/log_contents.html | 11 + .../templates/databases/logs/view_log.html | 11 + trove_dashboard/content/databases/urls.py | 7 +- trove_dashboard/test/test_data/trove_data.py | 44 +++ 14 files changed, 845 insertions(+), 3 deletions(-) create mode 100644 releasenotes/notes/logging-support-f999a1b1b342eb4d.yaml create mode 100644 trove_dashboard/content/databases/logs/__init__.py create mode 100644 trove_dashboard/content/databases/logs/tables.py create mode 100644 trove_dashboard/content/databases/logs/tests.py create mode 100644 trove_dashboard/content/databases/logs/urls.py create mode 100644 trove_dashboard/content/databases/logs/views.py create mode 100644 trove_dashboard/content/databases/templates/databases/logs/_detail_log.html create mode 100644 trove_dashboard/content/databases/templates/databases/logs/_log_contents.html create mode 100644 trove_dashboard/content/databases/templates/databases/logs/log_contents.html create mode 100644 trove_dashboard/content/databases/templates/databases/logs/view_log.html 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)