Add GUI logging workflows
The workflow works like this: 1. Drain the tripleo Zaqar queue 2. Format the messages as a log string 3. Publish the string to a swift object If the swift object exceeds 10MB, we rotate it. This patch also includes a workflow to download the latest log via a temporary url. The download log action shares a lot of code with the plan export action, so the common bits were extracted. Implements: spec gui-logging Implements: blueprint websocket-logging Change-Id: I2affd39e85ccfdbaa18590de182104715cfbbed4
This commit is contained in:
parent
fa0b9f5208
commit
4cd60846af
5
releasenotes/notes/gui-logging-5413d0d86e618c59.yaml
Normal file
5
releasenotes/notes/gui-logging-5413d0d86e618c59.yaml
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
features:
|
||||||
|
- |
|
||||||
|
GUI logging - we added actions and workflows to support processing and
|
||||||
|
storage of logging data from tripleo-ui
|
@ -94,6 +94,9 @@ mistral.actions =
|
|||||||
tripleo.plan.delete = tripleo_common.actions.plan:DeletePlanAction
|
tripleo.plan.delete = tripleo_common.actions.plan:DeletePlanAction
|
||||||
tripleo.plan.list = tripleo_common.actions.plan:ListPlansAction
|
tripleo.plan.list = tripleo_common.actions.plan:ListPlansAction
|
||||||
tripleo.plan.export = tripleo_common.actions.plan:ExportPlanAction
|
tripleo.plan.export = tripleo_common.actions.plan:ExportPlanAction
|
||||||
|
tripleo.logging_to_swift.format_messages = tripleo_common.actions.logging_to_swift:FormatMessagesAction
|
||||||
|
tripleo.logging_to_swift.publish_ui_log_to_swift = tripleo_common.actions.logging_to_swift:PublishUILogToSwiftAction
|
||||||
|
tripleo.logging_to_swift.prepare_log_download = tripleo_common.actions.logging_to_swift:PrepareLogDownloadAction
|
||||||
tripleo.role.list = tripleo_common.actions.plan:ListRolesAction
|
tripleo.role.list = tripleo_common.actions.plan:ListRolesAction
|
||||||
tripleo.scale.delete_node = tripleo_common.actions.scale:ScaleDownAction
|
tripleo.scale.delete_node = tripleo_common.actions.scale:ScaleDownAction
|
||||||
tripleo.swift.tempurl = tripleo_common.actions.swifthelper:SwiftTempUrlAction
|
tripleo.swift.tempurl = tripleo_common.actions.swifthelper:SwiftTempUrlAction
|
||||||
|
183
tripleo_common/actions/logging_to_swift.py
Normal file
183
tripleo_common/actions/logging_to_swift.py
Normal file
@ -0,0 +1,183 @@
|
|||||||
|
# Copyright 2017 Red Hat, Inc.
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
# 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 datetime import datetime
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import shutil
|
||||||
|
import tempfile
|
||||||
|
|
||||||
|
from mistral_lib import actions
|
||||||
|
from oslo_concurrency import processutils
|
||||||
|
from swiftclient import exceptions as swiftexceptions
|
||||||
|
|
||||||
|
from tripleo_common.actions import base
|
||||||
|
from tripleo_common import constants
|
||||||
|
from tripleo_common.utils import swift as swiftutils
|
||||||
|
from tripleo_common.utils import time_functions as timeutils
|
||||||
|
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class FormatMessagesAction(actions.Action):
|
||||||
|
"""Format messages as logs
|
||||||
|
|
||||||
|
Given a list of Zaqar messages from the TripleO UI, return a log-formatted
|
||||||
|
string
|
||||||
|
"""
|
||||||
|
def __init__(self, messages):
|
||||||
|
super(FormatMessagesAction, self).__init__()
|
||||||
|
self.messages = messages
|
||||||
|
|
||||||
|
def run(self, context):
|
||||||
|
lines = []
|
||||||
|
|
||||||
|
for zaqar_message in self.messages:
|
||||||
|
log_object = zaqar_message.get('body')
|
||||||
|
|
||||||
|
if not log_object:
|
||||||
|
continue
|
||||||
|
|
||||||
|
body = log_object.get('message', '')
|
||||||
|
level = log_object.get('level', 'info')
|
||||||
|
timestamp = log_object.get('timestamp', datetime.utcnow())
|
||||||
|
|
||||||
|
if isinstance(body, (dict, list,)):
|
||||||
|
body = json.dumps(body)
|
||||||
|
|
||||||
|
lines.append(
|
||||||
|
'{date} {level} {body}'.format(
|
||||||
|
date=timeutils.epoch_to_formatted_date(timestamp),
|
||||||
|
level=level,
|
||||||
|
body=body
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return '\n'.join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
class PublishUILogToSwiftAction(base.TripleOAction):
|
||||||
|
"""Publish logs from UI to Swift"""
|
||||||
|
|
||||||
|
def __init__(self, logging_data, logging_container):
|
||||||
|
super(PublishUILogToSwiftAction, self).__init__()
|
||||||
|
self.logging_data = logging_data
|
||||||
|
self.logging_container = logging_container
|
||||||
|
|
||||||
|
def _rotate(self, swift):
|
||||||
|
"""Optimistic log rotation
|
||||||
|
|
||||||
|
Failure to sucessfully complete log rotation doesn't cause the
|
||||||
|
entire action to fail
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
headers = swift.head_object(self.logging_container,
|
||||||
|
constants.TRIPLEO_UI_LOG_FILENAME)
|
||||||
|
if headers['content-length'] < constants.TRIPLEO_UI_LOG_FILE_SIZE:
|
||||||
|
LOG.debug("Log file hasn't reached a full size so it doesn't"
|
||||||
|
" need to be rotated.")
|
||||||
|
return
|
||||||
|
except swiftexceptions.ClientException:
|
||||||
|
LOG.debug("Couldn't get existing log file, skip log rotation.")
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
files = swift.get_container(self.logging_container)[1]
|
||||||
|
except swiftexceptions.ClientException:
|
||||||
|
LOG.warn("Logging container doesn't exist, skip log rotation.")
|
||||||
|
return
|
||||||
|
|
||||||
|
largest_existing_suffix = 0
|
||||||
|
|
||||||
|
for f in files:
|
||||||
|
try:
|
||||||
|
suffix = int(f['name'].split('.')[-1])
|
||||||
|
if suffix > largest_existing_suffix:
|
||||||
|
largest_existing_suffix = suffix
|
||||||
|
except ValueError:
|
||||||
|
continue
|
||||||
|
|
||||||
|
next_suffix = largest_existing_suffix + 1
|
||||||
|
next_filename = '{}.{}'.format(
|
||||||
|
constants.TRIPLEO_UI_LOG_FILENAME, next_suffix)
|
||||||
|
try:
|
||||||
|
data = swift.get_object(self.logging_container,
|
||||||
|
constants.TRIPLEO_UI_LOG_FILENAME)[1]
|
||||||
|
swift.put_object(self.logging_container, next_filename, data)
|
||||||
|
swift.delete_object(self.logging_container,
|
||||||
|
constants.TRIPLEO_UI_LOG_FILENAME)
|
||||||
|
except swiftexceptions.ClientException as err:
|
||||||
|
msg = "Log rotation failed: %s" % err
|
||||||
|
LOG.warn(msg)
|
||||||
|
|
||||||
|
def run(self, context):
|
||||||
|
swift = self.get_object_client(context)
|
||||||
|
swiftutils.get_or_create_container(swift, self.logging_container)
|
||||||
|
self._rotate(swift)
|
||||||
|
|
||||||
|
try:
|
||||||
|
old_contents = swift.get_object(
|
||||||
|
self.logging_container,
|
||||||
|
constants.TRIPLEO_UI_LOG_FILENAME)[1]
|
||||||
|
new_contents = old_contents + '\n' + self.logging_data
|
||||||
|
except swiftexceptions.ClientException:
|
||||||
|
LOG.debug(
|
||||||
|
"There is no existing logging data, starting a new file.")
|
||||||
|
new_contents = self.logging_data
|
||||||
|
|
||||||
|
try:
|
||||||
|
swift.put_object(self.logging_container,
|
||||||
|
constants.TRIPLEO_UI_LOG_FILENAME,
|
||||||
|
new_contents)
|
||||||
|
except swiftexceptions.ClientException as err:
|
||||||
|
msg = "Failed to publish logs: %s" % err
|
||||||
|
return actions.Result(error=msg)
|
||||||
|
|
||||||
|
|
||||||
|
class PrepareLogDownloadAction(base.TripleOAction):
|
||||||
|
"""Publish all GUI logs to a temporary URL"""
|
||||||
|
|
||||||
|
def __init__(self, logging_container, downloads_container, delete_after):
|
||||||
|
super(PrepareLogDownloadAction, self).__init__()
|
||||||
|
self.logging_container = logging_container
|
||||||
|
self.downloads_container = downloads_container
|
||||||
|
self.delete_after = delete_after
|
||||||
|
|
||||||
|
def run(self, context):
|
||||||
|
swift = self.get_object_client(context)
|
||||||
|
tmp_dir = tempfile.mkdtemp()
|
||||||
|
tarball_name = 'logs-%s.tar.gz' % timeutils.timestamp()
|
||||||
|
|
||||||
|
try:
|
||||||
|
swiftutils.download_container(
|
||||||
|
swift, self.logging_container, tmp_dir)
|
||||||
|
swiftutils.create_and_upload_tarball(
|
||||||
|
swift, tmp_dir, self.downloads_container,
|
||||||
|
tarball_name, self.delete_after)
|
||||||
|
except swiftexceptions.ClientException as err:
|
||||||
|
msg = "Error attempting an operation on container: %s" % err
|
||||||
|
return actions.Result(error=msg)
|
||||||
|
except (OSError, IOError) as err:
|
||||||
|
msg = "Error while writing file: %s" % err
|
||||||
|
return actions.Result(error=msg)
|
||||||
|
except processutils.ProcessExecutionError as err:
|
||||||
|
msg = "Error while creating a tarball: %s" % err
|
||||||
|
return actions.Result(error=msg)
|
||||||
|
except Exception as err:
|
||||||
|
msg = "Error exporting logs: %s" % err
|
||||||
|
return actions.Result(error=msg)
|
||||||
|
finally:
|
||||||
|
shutil.rmtree(tmp_dir)
|
||||||
|
|
||||||
|
return tarball_name
|
@ -13,7 +13,6 @@
|
|||||||
# 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
|
import logging
|
||||||
import os
|
|
||||||
import shutil
|
import shutil
|
||||||
import tempfile
|
import tempfile
|
||||||
import yaml
|
import yaml
|
||||||
@ -31,7 +30,6 @@ from tripleo_common import constants
|
|||||||
from tripleo_common import exception
|
from tripleo_common import exception
|
||||||
from tripleo_common.utils import plan as plan_utils
|
from tripleo_common.utils import plan as plan_utils
|
||||||
from tripleo_common.utils import swift as swiftutils
|
from tripleo_common.utils import swift as swiftutils
|
||||||
from tripleo_common.utils import tarball
|
|
||||||
from tripleo_common.utils.validations import pattern_validator
|
from tripleo_common.utils.validations import pattern_validator
|
||||||
|
|
||||||
|
|
||||||
@ -221,46 +219,16 @@ class ExportPlanAction(base.TripleOAction):
|
|||||||
self.delete_after = delete_after
|
self.delete_after = delete_after
|
||||||
self.exports_container = exports_container
|
self.exports_container = exports_container
|
||||||
|
|
||||||
def _download_templates(self, swift, tmp_dir):
|
|
||||||
"""Download templates to a temp folder."""
|
|
||||||
template_files = swift.get_container(self.plan)[1]
|
|
||||||
|
|
||||||
for tf in template_files:
|
|
||||||
filename = tf['name']
|
|
||||||
contents = swift.get_object(self.plan, filename)[1]
|
|
||||||
path = os.path.join(tmp_dir, filename)
|
|
||||||
dirname = os.path.dirname(path)
|
|
||||||
|
|
||||||
if not os.path.exists(dirname):
|
|
||||||
os.makedirs(dirname)
|
|
||||||
|
|
||||||
with open(path, 'w') as f:
|
|
||||||
f.write(contents)
|
|
||||||
|
|
||||||
def _create_and_upload_tarball(self, swift, tmp_dir):
|
|
||||||
"""Create a tarball containing the tmp_dir and upload it to Swift."""
|
|
||||||
tarball_name = '%s.tar.gz' % self.plan
|
|
||||||
headers = {'X-Delete-After': self.delete_after}
|
|
||||||
|
|
||||||
# make sure the root container which holds all plan exports exists
|
|
||||||
try:
|
|
||||||
swift.get_container(self.exports_container)
|
|
||||||
except swiftexceptions.ClientException:
|
|
||||||
swift.put_container(self.exports_container)
|
|
||||||
|
|
||||||
with tempfile.NamedTemporaryFile() as tmp_tarball:
|
|
||||||
tarball.create_tarball(tmp_dir, tmp_tarball.name)
|
|
||||||
|
|
||||||
swift.put_object(self.exports_container, tarball_name, tmp_tarball,
|
|
||||||
headers=headers)
|
|
||||||
|
|
||||||
def run(self, context):
|
def run(self, context):
|
||||||
swift = self.get_object_client(context)
|
swift = self.get_object_client(context)
|
||||||
tmp_dir = tempfile.mkdtemp()
|
tmp_dir = tempfile.mkdtemp()
|
||||||
|
tarball_name = '%s.tar.gz' % self.plan
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self._download_templates(swift, tmp_dir)
|
swiftutils.download_container(swift, self.plan, tmp_dir)
|
||||||
self._create_and_upload_tarball(swift, tmp_dir)
|
swiftutils.create_and_upload_tarball(
|
||||||
|
swift, tmp_dir, self.exports_container, tarball_name,
|
||||||
|
self.delete_after)
|
||||||
except swiftexceptions.ClientException as err:
|
except swiftexceptions.ClientException as err:
|
||||||
msg = "Error attempting an operation on container: %s" % err
|
msg = "Error attempting an operation on container: %s" % err
|
||||||
return actions.Result(error=msg)
|
return actions.Result(error=msg)
|
||||||
|
@ -127,3 +127,6 @@ DEFAULT_DEPLOY_RAMDISK_NAME = 'bm-deploy-ramdisk'
|
|||||||
|
|
||||||
# The name for the swift container to host the cache for tripleo
|
# The name for the swift container to host the cache for tripleo
|
||||||
TRIPLEO_CACHE_CONTAINER = "__cache__"
|
TRIPLEO_CACHE_CONTAINER = "__cache__"
|
||||||
|
|
||||||
|
TRIPLEO_UI_LOG_FILE_SIZE = 1e7 # 10MB
|
||||||
|
TRIPLEO_UI_LOG_FILENAME = 'tripleo-ui.logs'
|
||||||
|
161
tripleo_common/tests/actions/test_logging_to_swift.py
Normal file
161
tripleo_common/tests/actions/test_logging_to_swift.py
Normal file
@ -0,0 +1,161 @@
|
|||||||
|
# Copyright 2017 Red Hat, Inc.
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
# 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 mock
|
||||||
|
|
||||||
|
from oslo_concurrency import processutils
|
||||||
|
|
||||||
|
from tripleo_common.actions import logging_to_swift
|
||||||
|
from tripleo_common.tests import base
|
||||||
|
|
||||||
|
|
||||||
|
class LogFormattingTest(base.TestCase):
|
||||||
|
|
||||||
|
def test_log_formatting(self):
|
||||||
|
messages = [
|
||||||
|
{
|
||||||
|
'body': {
|
||||||
|
'message': 'Test 1',
|
||||||
|
'level': 'INFO',
|
||||||
|
'timestamp': '1496322000000'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'body': {
|
||||||
|
'message': 'Test 2',
|
||||||
|
'level': 'WARN',
|
||||||
|
'timestamp': '1496329200000'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
action = logging_to_swift.FormatMessagesAction(messages)
|
||||||
|
result = action.run({})
|
||||||
|
self.assertEqual(result, ('2017-06-01 13:00:00 INFO Test 1\n'
|
||||||
|
'2017-06-01 15:00:00 WARN Test 2'))
|
||||||
|
|
||||||
|
|
||||||
|
class PublishUILogToSwiftActionTest(base.TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(PublishUILogToSwiftActionTest, self).setUp()
|
||||||
|
self.container = 'container'
|
||||||
|
self.swift = mock.MagicMock()
|
||||||
|
swift_patcher = mock.patch(
|
||||||
|
'tripleo_common.actions.base.TripleOAction.get_object_client',
|
||||||
|
return_value=self.swift)
|
||||||
|
swift_patcher.start()
|
||||||
|
self.addCleanup(swift_patcher.stop)
|
||||||
|
self.ctx = mock.MagicMock()
|
||||||
|
|
||||||
|
def test_simple_success(self):
|
||||||
|
self.swift.head_object.return_value = {
|
||||||
|
'content-length': 1
|
||||||
|
}
|
||||||
|
self.swift.get_container.return_value = (
|
||||||
|
{}, []
|
||||||
|
)
|
||||||
|
data = 'data'
|
||||||
|
action = logging_to_swift.PublishUILogToSwiftAction(
|
||||||
|
data, self.container)
|
||||||
|
action.run(self.ctx)
|
||||||
|
|
||||||
|
self.swift.get_object.assert_called_once()
|
||||||
|
self.swift.head_object.assert_called_once()
|
||||||
|
self.swift.put_object.assert_called_once()
|
||||||
|
self.swift.get_container.assert_called_once()
|
||||||
|
|
||||||
|
def test_rotate(self):
|
||||||
|
self.swift.head_object.return_value = {
|
||||||
|
'content-length': 2e7
|
||||||
|
}
|
||||||
|
self.swift.get_container.return_value = (
|
||||||
|
{}, []
|
||||||
|
)
|
||||||
|
|
||||||
|
old_data = 'old data'
|
||||||
|
new_data = 'new data'
|
||||||
|
result = old_data + '\n' + new_data
|
||||||
|
|
||||||
|
self.swift.get_object.return_value = ({}, old_data)
|
||||||
|
|
||||||
|
action = logging_to_swift.PublishUILogToSwiftAction(
|
||||||
|
new_data, self.container)
|
||||||
|
action.run(self.ctx)
|
||||||
|
|
||||||
|
self.swift.head_object.assert_called_once()
|
||||||
|
self.swift.put_object.assert_called_with(
|
||||||
|
self.container,
|
||||||
|
'tripleo-ui.logs',
|
||||||
|
result
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class PrepareLogDownloadActionTest(base.TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(PrepareLogDownloadActionTest, self).setUp()
|
||||||
|
self.log_files = (
|
||||||
|
'tripleo-ui.logs.2',
|
||||||
|
'tripleo-ui.logs.1',
|
||||||
|
'tripleo-ui.logs'
|
||||||
|
)
|
||||||
|
self.swift = mock.MagicMock()
|
||||||
|
self.swift.get_container.return_value = (
|
||||||
|
{'x-container-meta-usage-tripleo': 'plan'}, [
|
||||||
|
{'name': lf} for lf in self.log_files
|
||||||
|
]
|
||||||
|
)
|
||||||
|
self.swift.get_object.return_value = ({}, 'log content')
|
||||||
|
swift_patcher = mock.patch(
|
||||||
|
'tripleo_common.actions.base.TripleOAction.get_object_client',
|
||||||
|
return_value=self.swift)
|
||||||
|
swift_patcher.start()
|
||||||
|
self.addCleanup(swift_patcher.stop)
|
||||||
|
|
||||||
|
self.ctx = mock.MagicMock()
|
||||||
|
|
||||||
|
@mock.patch('tripleo_common.utils.tarball.create_tarball')
|
||||||
|
@mock.patch('tempfile.mkdtemp')
|
||||||
|
def test_run_success(self, mock_mkdtemp, mock_create_tarball):
|
||||||
|
get_object_mock_calls = [
|
||||||
|
mock.call('logging-container', lf) for lf in self.log_files
|
||||||
|
]
|
||||||
|
get_container_mock_calls = [
|
||||||
|
mock.call('logging-container')
|
||||||
|
]
|
||||||
|
mock_mkdtemp.return_value = '/tmp/test123'
|
||||||
|
|
||||||
|
action = logging_to_swift.PrepareLogDownloadAction(
|
||||||
|
'logging-container', 'downloads-container', 3600
|
||||||
|
)
|
||||||
|
|
||||||
|
action.run(self.ctx)
|
||||||
|
|
||||||
|
self.swift.get_container.assert_has_calls(get_container_mock_calls)
|
||||||
|
self.swift.get_object.assert_has_calls(
|
||||||
|
get_object_mock_calls, any_order=True)
|
||||||
|
mock_create_tarball.assert_called_once()
|
||||||
|
|
||||||
|
@mock.patch('tripleo_common.utils.tarball.create_tarball')
|
||||||
|
def test_run_error_creating_tarball(self, mock_create_tarball):
|
||||||
|
mock_create_tarball.side_effect = processutils.ProcessExecutionError
|
||||||
|
|
||||||
|
action = logging_to_swift.PrepareLogDownloadAction(
|
||||||
|
'logging-container', 'downloads-container', 3600
|
||||||
|
)
|
||||||
|
|
||||||
|
result = action.run(self.ctx)
|
||||||
|
|
||||||
|
error = "Error while creating a tarball"
|
||||||
|
self.assertIn(error, result.error)
|
@ -15,6 +15,8 @@
|
|||||||
|
|
||||||
import mock
|
import mock
|
||||||
|
|
||||||
|
from swiftclient import exceptions as swiftexceptions
|
||||||
|
|
||||||
from tripleo_common.tests import base
|
from tripleo_common.tests import base
|
||||||
from tripleo_common.utils import swift as swift_utils
|
from tripleo_common.utils import swift as swift_utils
|
||||||
|
|
||||||
@ -75,3 +77,13 @@ class SwiftTest(base.TestCase):
|
|||||||
self.swiftclient.get_account.assert_called()
|
self.swiftclient.get_account.assert_called()
|
||||||
self.swiftclient.get_container.assert_called()
|
self.swiftclient.get_container.assert_called()
|
||||||
self.swiftclient.delete_object.assert_not_called()
|
self.swiftclient.delete_object.assert_not_called()
|
||||||
|
|
||||||
|
def test_get_or_create_container_create(self):
|
||||||
|
self.swiftclient.get_container.side_effect = \
|
||||||
|
swiftexceptions.ClientException('error')
|
||||||
|
swift_utils.get_or_create_container(self.swiftclient, 'abc')
|
||||||
|
self.swiftclient.put_container.assert_called()
|
||||||
|
|
||||||
|
def test_get_or_create_container_get(self):
|
||||||
|
swift_utils.get_or_create_container(self.swiftclient, 'abc')
|
||||||
|
self.swiftclient.put_container.assert_not_called()
|
||||||
|
83
tripleo_common/tests/utils/test_time_functions.py
Normal file
83
tripleo_common/tests/utils/test_time_functions.py
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
# Copyright (c) 2017 Red Hat, 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 contextlib
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
from tripleo_common.tests import base
|
||||||
|
from tripleo_common.utils import time_functions
|
||||||
|
|
||||||
|
|
||||||
|
@contextlib.contextmanager
|
||||||
|
def mock_now(dt):
|
||||||
|
"""Context manager for mocking out datetime.utcnow() in unit tests.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
with mock_now(datetime.datetime(2011, 2, 3, 10, 11)):
|
||||||
|
assert datetime.datetime.utcnow() \
|
||||||
|
== datetime.datetime(2011, 2, 3, 10, 11)
|
||||||
|
"""
|
||||||
|
class MockDatetime(datetime.datetime):
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def utcnow(cls):
|
||||||
|
return dt
|
||||||
|
|
||||||
|
real_datetime = datetime.datetime
|
||||||
|
datetime.datetime = MockDatetime
|
||||||
|
|
||||||
|
try:
|
||||||
|
yield datetime.datetime
|
||||||
|
finally:
|
||||||
|
datetime.datetime = real_datetime
|
||||||
|
|
||||||
|
|
||||||
|
class TimeFunctionsTest(base.TestCase):
|
||||||
|
|
||||||
|
def test_timestamp(self):
|
||||||
|
fake_date = datetime.datetime(2017, 7, 31, 13, 0, 0)
|
||||||
|
with mock_now(fake_date):
|
||||||
|
self.assertEqual(time_functions.timestamp(), '20170731-130000')
|
||||||
|
|
||||||
|
def test_epoch_formatting(self):
|
||||||
|
self.assertEqual(
|
||||||
|
time_functions.epoch_to_formatted_date(1000),
|
||||||
|
'1970-01-01 00:00:01')
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
time_functions.epoch_to_formatted_date(1000 * 60),
|
||||||
|
'1970-01-01 00:01:00')
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
time_functions.epoch_to_formatted_date(1000 * 60 * 60 * 24),
|
||||||
|
'1970-01-02 00:00:00')
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
time_functions.epoch_to_formatted_date(1000.0),
|
||||||
|
'1970-01-01 00:00:01')
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
time_functions.epoch_to_formatted_date('1000'),
|
||||||
|
'1970-01-01 00:00:01')
|
||||||
|
|
||||||
|
self.assertRaises(
|
||||||
|
ValueError,
|
||||||
|
time_functions.epoch_to_formatted_date,
|
||||||
|
'abc')
|
||||||
|
|
||||||
|
self.assertRaises(
|
||||||
|
TypeError,
|
||||||
|
time_functions.epoch_to_formatted_date,
|
||||||
|
None)
|
@ -14,7 +14,16 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import tempfile
|
||||||
|
|
||||||
|
from swiftclient import exceptions as swiftexceptions
|
||||||
|
|
||||||
from tripleo_common import constants
|
from tripleo_common import constants
|
||||||
|
from tripleo_common.utils import tarball
|
||||||
|
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def empty_container(swiftclient, name):
|
def empty_container(swiftclient, name):
|
||||||
@ -43,3 +52,45 @@ def empty_container(swiftclient, name):
|
|||||||
def delete_container(swiftclient, name):
|
def delete_container(swiftclient, name):
|
||||||
empty_container(swiftclient, name)
|
empty_container(swiftclient, name)
|
||||||
swiftclient.delete_container(name)
|
swiftclient.delete_container(name)
|
||||||
|
|
||||||
|
|
||||||
|
def download_container(swiftclient, container, dest):
|
||||||
|
"""Download the contents of a Swift container to a directory"""
|
||||||
|
|
||||||
|
objects = swiftclient.get_container(container)[1]
|
||||||
|
|
||||||
|
for obj in objects:
|
||||||
|
filename = obj['name']
|
||||||
|
contents = swiftclient.get_object(container, filename)[1]
|
||||||
|
path = os.path.join(dest, filename)
|
||||||
|
dirname = os.path.dirname(path)
|
||||||
|
|
||||||
|
if not os.path.exists(dirname):
|
||||||
|
os.makedirs(dirname)
|
||||||
|
|
||||||
|
with open(path, 'w') as f:
|
||||||
|
f.write(contents)
|
||||||
|
|
||||||
|
|
||||||
|
def get_or_create_container(swiftclient, container):
|
||||||
|
try:
|
||||||
|
return swiftclient.get_container(container)
|
||||||
|
except swiftexceptions.ClientException:
|
||||||
|
LOG.debug("Container %s doesn't exist, creating...", container)
|
||||||
|
return swiftclient.put_container(container)
|
||||||
|
|
||||||
|
|
||||||
|
def create_and_upload_tarball(swiftclient,
|
||||||
|
tmp_dir,
|
||||||
|
container,
|
||||||
|
tarball_name,
|
||||||
|
delete_after=3600):
|
||||||
|
"""Create a tarball containing the tmp_dir and upload it to Swift."""
|
||||||
|
headers = {'X-Delete-After': delete_after}
|
||||||
|
|
||||||
|
get_or_create_container(swiftclient, container)
|
||||||
|
|
||||||
|
with tempfile.NamedTemporaryFile() as tmp_tarball:
|
||||||
|
tarball.create_tarball(tmp_dir, tmp_tarball.name)
|
||||||
|
swiftclient.put_object(container, tarball_name, tmp_tarball,
|
||||||
|
headers=headers)
|
||||||
|
29
tripleo_common/utils/time_functions.py
Normal file
29
tripleo_common/utils/time_functions.py
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
# Copyright 2017 Red Hat, Inc.
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
# 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 datetime
|
||||||
|
|
||||||
|
|
||||||
|
def timestamp():
|
||||||
|
"""Return a UTC-now timestamp as a string"""
|
||||||
|
return datetime.datetime.utcnow().strftime('%Y%m%d-%H%M%S')
|
||||||
|
|
||||||
|
|
||||||
|
def epoch_to_formatted_date(epoch):
|
||||||
|
"""Convert an epoch time to a string"""
|
||||||
|
epoch = float(epoch) / 1000
|
||||||
|
dt = datetime.datetime.utcfromtimestamp(epoch)
|
||||||
|
return dt.strftime('%Y-%m-%d %H:%M:%S')
|
@ -462,3 +462,140 @@ workflows:
|
|||||||
deprecated: <% $.get('formatted', []) %>
|
deprecated: <% $.get('formatted', []) %>
|
||||||
on-success:
|
on-success:
|
||||||
- fail: <% $.get('status') = "FAILED" %>
|
- fail: <% $.get('status') = "FAILED" %>
|
||||||
|
|
||||||
|
publish_ui_logs_to_swift:
|
||||||
|
description: >
|
||||||
|
This workflow drains a zaqar queue, and publish its messages into a log
|
||||||
|
file in swift. This workflow is called by cron trigger.
|
||||||
|
|
||||||
|
input:
|
||||||
|
- queue_name: tripleo
|
||||||
|
- logging_queue_name: tripleo-ui-logging
|
||||||
|
- logging_container: tripleo-ui-logs
|
||||||
|
|
||||||
|
tasks:
|
||||||
|
|
||||||
|
get_messages:
|
||||||
|
action: zaqar.queue_messages
|
||||||
|
on-success: format_messages
|
||||||
|
on-error: get_messages_set_status_failed
|
||||||
|
input:
|
||||||
|
queue_name: <% $.logging_queue_name %>
|
||||||
|
|
||||||
|
publish:
|
||||||
|
messages: <% task(get_messages).result %>
|
||||||
|
|
||||||
|
format_messages:
|
||||||
|
action: tripleo.logging_to_swift.format_messages
|
||||||
|
on-success: upload_to_swift
|
||||||
|
input:
|
||||||
|
messages: <% $.messages %>
|
||||||
|
|
||||||
|
publish:
|
||||||
|
messages: <% task(format_messages).result %>
|
||||||
|
|
||||||
|
upload_to_swift:
|
||||||
|
action: tripleo.logging_to_swift.publish_ui_log_to_swift
|
||||||
|
on-success: set_status_success
|
||||||
|
on-error: upload_to_swift_set_status_failed
|
||||||
|
input:
|
||||||
|
logging_data: <% $.messages %>
|
||||||
|
logging_container: <% $.logging_container %>
|
||||||
|
|
||||||
|
set_status_success:
|
||||||
|
on-success: notify_zaqar
|
||||||
|
publish:
|
||||||
|
status: SUCCESS
|
||||||
|
message: <% task(upload_to_swift).result %>
|
||||||
|
|
||||||
|
upload_to_swift_set_status_failed:
|
||||||
|
on-success: notify_zaqar
|
||||||
|
publish:
|
||||||
|
status: FAILED
|
||||||
|
message: <% task(upload_to_swift).result %>
|
||||||
|
|
||||||
|
get_messages_set_status_failed:
|
||||||
|
on-success: notify_zaqar
|
||||||
|
publish:
|
||||||
|
status: FAILED
|
||||||
|
message: <% task(get_messages).result %>
|
||||||
|
|
||||||
|
notify_zaqar:
|
||||||
|
action: zaqar.queue_post
|
||||||
|
input:
|
||||||
|
queue_name: <% $.queue_name %>
|
||||||
|
messages:
|
||||||
|
body:
|
||||||
|
type: tripleo.plan_management.v1.publish_ui_logs_to_swift
|
||||||
|
payload:
|
||||||
|
status: <% $.status %>
|
||||||
|
message: <% $.get('message', '') %>
|
||||||
|
execution: <% execution() %>
|
||||||
|
on-success:
|
||||||
|
- fail: <% $.get('status') = "FAILED" %>
|
||||||
|
|
||||||
|
download_logs:
|
||||||
|
description: Creates a tarball with logging data
|
||||||
|
input:
|
||||||
|
- queue_name: tripleo
|
||||||
|
- logging_container: "tripleo-ui-logs"
|
||||||
|
- downloads_container: "tripleo-ui-logs-downloads"
|
||||||
|
- delete_after: 3600
|
||||||
|
|
||||||
|
tasks:
|
||||||
|
|
||||||
|
prepare_log_download:
|
||||||
|
action: tripleo.logging_to_swift.prepare_log_download
|
||||||
|
input:
|
||||||
|
logging_container: <% $.logging_container %>
|
||||||
|
downloads_container: <% $.downloads_container %>
|
||||||
|
delete_after: <% $.delete_after %>
|
||||||
|
on-success: create_tempurl
|
||||||
|
on-error: download_logs_set_status_failed
|
||||||
|
publish:
|
||||||
|
filename: <% task(prepare_log_download).result %>
|
||||||
|
|
||||||
|
create_tempurl:
|
||||||
|
action: tripleo.swift.tempurl
|
||||||
|
on-success: set_status_success
|
||||||
|
on-error: create_tempurl_set_status_failed
|
||||||
|
input:
|
||||||
|
container: <% $.downloads_container %>
|
||||||
|
obj: <% $.filename %>
|
||||||
|
valid: 3600
|
||||||
|
publish:
|
||||||
|
tempurl: <% task(create_tempurl).result %>
|
||||||
|
|
||||||
|
set_status_success:
|
||||||
|
on-success: notify_zaqar
|
||||||
|
publish:
|
||||||
|
status: SUCCESS
|
||||||
|
message: <% task(create_tempurl).result %>
|
||||||
|
tempurl: <% task(create_tempurl).result %>
|
||||||
|
|
||||||
|
download_logs_set_status_failed:
|
||||||
|
on-success: notify_zaqar
|
||||||
|
publish:
|
||||||
|
status: FAILED
|
||||||
|
message: <% task(prepare_log_download).result %>
|
||||||
|
|
||||||
|
create_tempurl_set_status_failed:
|
||||||
|
on-success: notify_zaqar
|
||||||
|
publish:
|
||||||
|
status: FAILED
|
||||||
|
message: <% task(create_tempurl).result %>
|
||||||
|
|
||||||
|
notify_zaqar:
|
||||||
|
action: zaqar.queue_post
|
||||||
|
input:
|
||||||
|
queue_name: <% $.queue_name %>
|
||||||
|
messages:
|
||||||
|
body:
|
||||||
|
type: tripleo.plan_management.v1.download_logs
|
||||||
|
payload:
|
||||||
|
status: <% $.status %>
|
||||||
|
message: <% $.get('message', '') %>
|
||||||
|
execution: <% execution() %>
|
||||||
|
tempurl: <% $.get('tempurl', '') %>
|
||||||
|
on-success:
|
||||||
|
- fail: <% $.get('status') = "FAILED" %>
|
||||||
|
Loading…
Reference in New Issue
Block a user