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
This commit is contained in:
parent
9ad5737ea2
commit
55bd481865
5
releasenotes/notes/logging-support-f999a1b1b342eb4d.yaml
Normal file
5
releasenotes/notes/logging-support-f999a1b1b342eb4d.yaml
Normal file
@ -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.
|
@ -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)
|
||||
|
0
trove_dashboard/content/databases/logs/__init__.py
Normal file
0
trove_dashboard/content/databases/logs/__init__.py
Normal file
156
trove_dashboard/content/databases/logs/tables.py
Normal file
156
trove_dashboard/content/databases/logs/tables.py
Normal file
@ -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
|
361
trove_dashboard/content/databases/logs/tests.py
Normal file
361
trove_dashboard/content/databases/logs/tests.py
Normal file
@ -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")
|
32
trove_dashboard/content/databases/logs/urls.py
Normal file
32
trove_dashboard/content/databases/logs/urls.py
Normal file
@ -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<filename>[^/]+)/%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'),
|
||||
)
|
129
trove_dashboard/content/databases/logs/views.py
Normal file
129
trove_dashboard/content/databases/logs/views.py
Normal file
@ -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())
|
@ -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
|
||||
|
@ -0,0 +1,22 @@
|
||||
{% load i18n %}
|
||||
{% load url from future %}
|
||||
|
||||
<div class="col-sm-12">
|
||||
<div class="clearfix">
|
||||
<form id="tail_length" action="{% url 'horizon:project:databases:logs:console' instance_id filename %}" class="form-inline pull-right">
|
||||
<label for="tail_length_select">{% trans "Log Length" %}</label>
|
||||
<input class="span1" type="text" name="length" value="{{ log_length }}" />
|
||||
<label for="publish_check">{% trans "Publish" %}</label>
|
||||
<input type="checkbox" name="publish" value="publish" {{ publish }}>
|
||||
<button class="btn btn-default btn-sm btn-primary always-enabled" type="submit">{% trans "Go" %}</button>
|
||||
<a href="{% url 'horizon:project:databases:detail' instance_id %}" class="btn btn-default btn-sm pull-right secondary">{% trans "Return to Log List" %}</a>
|
||||
<a href="{% url 'horizon:project:databases:logs:download_log' instance_id filename %}" class="btn btn-default btn-sm pull-right">{% trans "Download" %}</a>
|
||||
<a href="{% url 'horizon:project:databases:logs:full_log' instance_id filename %}" class="btn btn-default btn-sm pull-right" target="_blank">{% trans "View Full Log" %}</a>
|
||||
</form>
|
||||
</div>
|
||||
<br>
|
||||
|
||||
<pre class="logs">
|
||||
{{ log_contents }}
|
||||
</pre>
|
||||
</div>
|
@ -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 %}
|
||||
<pre class="logs">
|
||||
{{ log.data }}
|
||||
</pre>
|
||||
{% endblock %}
|
||||
|
||||
{% block modal-footer %}
|
||||
<a href="{% url 'horizon:project:databases:index' %}" class="btn btn-default secondary cancel close">{% trans "Close" %}</a>
|
||||
{% endblock %}
|
@ -0,0 +1,11 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load i18n %}
|
||||
{% block title %}{% trans "Log Contents" %}{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
<div class="row-fluid">
|
||||
<div class="col-sm-12">
|
||||
{% include "project/databases/logs/_log_contents.html" %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
@ -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 %}
|
@ -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<instance_id>[^/]+)/%s$'
|
||||
BASEINSTANCES = r'^(?P<instance_id>[^/]+)/%s'
|
||||
INSTANCES = BASEINSTANCES + '$'
|
||||
USERS = r'^(?P<instance_id>[^/]+)/(?P<user_name>[^/]+)/%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')),
|
||||
)
|
||||
|
@ -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)
|
||||
|
Loading…
Reference in New Issue
Block a user