Merge "Trove add support for log retrieval"
This commit is contained in:
commit
1c05349cb0
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):
|
def datastore_version_list(request, datastore):
|
||||||
return troveclient(request).datastore_versions.list(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
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
from django import template
|
from django import template
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
@ -19,9 +21,13 @@ from horizon import exceptions
|
|||||||
from horizon import tabs
|
from horizon import tabs
|
||||||
from trove_dashboard import api
|
from trove_dashboard import api
|
||||||
from trove_dashboard.content.databases import db_capability
|
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
|
from trove_dashboard.content.databases import tables
|
||||||
|
|
||||||
|
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class OverviewTab(tabs.Tab):
|
class OverviewTab(tabs.Tab):
|
||||||
name = _("Overview")
|
name = _("Overview")
|
||||||
slug = "overview"
|
slug = "overview"
|
||||||
@ -136,7 +142,26 @@ class BackupsTab(tabs.TableTab):
|
|||||||
return request.user.has_perm('openstack.services.object-store')
|
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):
|
class InstanceDetailTabs(tabs.TabGroup):
|
||||||
slug = "instance_details"
|
slug = "instance_details"
|
||||||
tabs = (OverviewTab, UserTab, DatabaseTab, BackupsTab)
|
tabs = (OverviewTab, UserTab, DatabaseTab, BackupsTab, LogsTab)
|
||||||
sticky = True
|
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
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
|
from django.conf.urls import include
|
||||||
from django.conf.urls import patterns
|
from django.conf.urls import patterns
|
||||||
from django.conf.urls import url
|
from django.conf.urls import url
|
||||||
|
|
||||||
|
from trove_dashboard.content.databases.logs import urls as logs_urls
|
||||||
from trove_dashboard.content.databases import views
|
from trove_dashboard.content.databases import views
|
||||||
|
|
||||||
|
BASEINSTANCES = r'^(?P<instance_id>[^/]+)/%s'
|
||||||
INSTANCES = r'^(?P<instance_id>[^/]+)/%s$'
|
INSTANCES = BASEINSTANCES + '$'
|
||||||
USERS = r'^(?P<instance_id>[^/]+)/(?P<user_name>[^/]+)/%s$'
|
USERS = r'^(?P<instance_id>[^/]+)/(?P<user_name>[^/]+)/%s$'
|
||||||
|
|
||||||
|
|
||||||
@ -44,4 +46,5 @@ urlpatterns = patterns(
|
|||||||
name='promote_to_replica_source'),
|
name='promote_to_replica_source'),
|
||||||
url(INSTANCES % 'manage_root', views.ManageRootView.as_view(),
|
url(INSTANCES % 'manage_root', views.ManageRootView.as_view(),
|
||||||
name='manage_root'),
|
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"
|
"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):
|
def data(TEST):
|
||||||
cluster1 = clusters.Cluster(clusters.Clusters(None),
|
cluster1 = clusters.Cluster(clusters.Clusters(None),
|
||||||
@ -413,6 +449,11 @@ def data(TEST):
|
|||||||
DatastoreVersion(datastores.DatastoreVersions(None),
|
DatastoreVersion(datastores.DatastoreVersions(None),
|
||||||
VERSION_VERTICA_7_1)
|
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 = utils.TestDataContainer()
|
||||||
TEST.trove_clusters.add(cluster1)
|
TEST.trove_clusters.add(cluster1)
|
||||||
TEST.trove_clusters.add(cluster2)
|
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_redis_3_0)
|
||||||
TEST.datastore_versions.add(version_mongodb_2_6)
|
TEST.datastore_versions.add(version_mongodb_2_6)
|
||||||
TEST.datastore_versions.add(version1)
|
TEST.datastore_versions.add(version1)
|
||||||
|
|
||||||
|
TEST.logs = utils.TestDataContainer()
|
||||||
|
TEST.logs.add(log1, log2, log3, log4)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user