diff --git a/config.py b/config.py index 39a257b15..e475be9c7 100644 --- a/config.py +++ b/config.py @@ -187,3 +187,11 @@ class DefaultConfig(object): # For enterprise: MAXIMUM_REPOSITORY_USAGE = 20 + + # System logs. + SYSTEM_LOGS_PATH = "/var/log/" + SYSTEM_SERVICE_LOGS_PATH = "/var/log/%s/current" + SYSTEM_SERVICES_PATH = "conf/init/" + + # Services that should not be shown in the logs view. + SYSTEM_SERVICE_BLACKLIST = ['tutumdocker', 'dockerfilebuild'] \ No newline at end of file diff --git a/endpoints/api/superuser.py b/endpoints/api/superuser.py index 17b70a630..8aee02f94 100644 --- a/endpoints/api/superuser.py +++ b/endpoints/api/superuser.py @@ -23,12 +23,14 @@ import features logger = logging.getLogger(__name__) -LOGS_PATH = "/var/log/%s/current" -SERVICES_PATH = "conf/init/" - def get_immediate_subdirectories(directory): return [name for name in os.listdir(directory) if os.path.isdir(os.path.join(directory, name))] +def get_services(): + services = set(get_immediate_subdirectories(app.config['SYSTEM_SERVICES_PATH'])) + services = services - set(app.config['SYSTEM_SERVICE_BLACKLIST']) + return services + @resource('/v1/superuser/systemlogs/') @internal_only @@ -39,12 +41,11 @@ class SuperUserGetLogsForService(ApiResource): def get(self, service): """ Returns the logs for the specific service. """ if SuperUserPermission().can(): - services = get_immediate_subdirectories(SERVICES_PATH) - if not service in services: + if not service in get_services(): abort(404) try: - with open(LOGS_PATH % service, 'r') as f: + with open(app.config['SYSTEM_SERVICE_LOGS_PATH'] % service, 'r') as f: logs = f.read() except Exception as ex: logger.exception('Cannot read logs') @@ -67,7 +68,7 @@ class SuperUserSystemLogServices(ApiResource): """ List the system logs for the current system. """ if SuperUserPermission().can(): return { - 'services': get_immediate_subdirectories(SERVICES_PATH) + 'services': list(get_services()) } abort(403) diff --git a/endpoints/csrf.py b/endpoints/csrf.py index b4b40d17c..39a0d636b 100644 --- a/endpoints/csrf.py +++ b/endpoints/csrf.py @@ -19,19 +19,21 @@ def generate_csrf_token(): return session['_csrf_token'] +def verify_csrf(): + token = session.get('_csrf_token', None) + found_token = request.values.get('_csrf_token', None) + + if not token or token != found_token: + msg = 'CSRF Failure. Session token was %s and request token was %s' + logger.error(msg, token, found_token) + abort(403, message='CSRF token was invalid or missing.') def csrf_protect(func): @wraps(func) def wrapper(*args, **kwargs): oauth_token = get_validated_oauth_token() if oauth_token is None and request.method != "GET" and request.method != "HEAD": - token = session.get('_csrf_token', None) - found_token = request.values.get('_csrf_token', None) - - if not token or token != found_token: - msg = 'CSRF Failure. Session token was %s and request token was %s' - logger.error(msg, token, found_token) - abort(403, message='CSRF token was invalid or missing.') + verify_csrf() return func(*args, **kwargs) return wrapper diff --git a/endpoints/web.py b/endpoints/web.py index 4717f7d40..d2f63f026 100644 --- a/endpoints/web.py +++ b/endpoints/web.py @@ -12,15 +12,18 @@ from data import model from data.model.oauth import DatabaseAuthorizationProvider from app import app, billing as stripe, build_logs, avatar from auth.auth import require_session_login, process_oauth -from auth.permissions import AdministerOrganizationPermission, ReadRepositoryPermission +from auth.permissions import (AdministerOrganizationPermission, ReadRepositoryPermission, + SuperUserPermission) + from util.invoice import renderInvoiceToPdf from util.seo import render_snapshot from util.cache import no_cache from endpoints.common import common_login, render_page_template, route_show_if, param_required -from endpoints.csrf import csrf_protect, generate_csrf_token +from endpoints.csrf import csrf_protect, generate_csrf_token, verify_csrf from endpoints.registry import set_cache_headers from util.names import parse_repository_name from util.useremails import send_email_changed +from util.systemlogs import build_logs_archive from auth import scopes import features @@ -466,3 +469,21 @@ def exchange_code_for_token(): provider = FlaskAuthorizationProvider() return provider.get_token(grant_type, client_id, client_secret, redirect_uri, code, scope=scope) + + +@web.route('/systemlogsarchive', methods=['GET']) +@process_oauth +@route_show_if(features.SUPER_USERS) +@no_cache +def download_logs_archive(): + # Note: We cannot use the decorator here because this is a GET method. That being said, this + # information is sensitive enough that we want the extra protection. + verify_csrf() + + if SuperUserPermission().can(): + archive_data = build_logs_archive(app) + return Response(archive_data, + mimetype="application/octet-stream", + headers={"Content-Disposition": "attachment;filename=erlogs.tar.gz"}) + + abort(403) diff --git a/static/css/quay.css b/static/css/quay.css index e2ac8b138..a21db2fbe 100644 --- a/static/css/quay.css +++ b/static/css/quay.css @@ -4879,4 +4879,10 @@ i.slack-icon { .system-log-download-panel { padding: 20px; + text-align: center; + font-size: 18px; +} + +.system-log-download-panel a { + margin-top: 20px; } diff --git a/static/js/controllers.js b/static/js/controllers.js index 9abbec906..653ec376b 100644 --- a/static/js/controllers.js +++ b/static/js/controllers.js @@ -2826,6 +2826,7 @@ function SuperUserAdminCtrl($scope, $timeout, ApiService, Features, UserService, $scope.debugLogs = null; $scope.pollChannel = null; $scope.logsScrolled = false; + $scope.csrf_token = window.__token; $scope.viewSystemLogs = function(service) { if ($scope.pollChannel) { @@ -2835,7 +2836,7 @@ function SuperUserAdminCtrl($scope, $timeout, ApiService, Features, UserService, $scope.debugService = service; $scope.debugLogs = null; - $scope.pollChannel = AngularPollChannel.create($scope, $scope.loadServiceLogs, 1 * 1000 /* 1s */); + $scope.pollChannel = AngularPollChannel.create($scope, $scope.loadServiceLogs, 2 * 1000 /* 2s */); $scope.pollChannel.start(); }; diff --git a/static/js/core-ui.js b/static/js/core-ui.js index f9349f855..2cd547f4d 100644 --- a/static/js/core-ui.js +++ b/static/js/core-ui.js @@ -20,7 +20,6 @@ angular.module("core-ui", []) if (isAnimatedScrolling) { return; } var element = $element.find("#co-log-viewer")[0]; isScrollBottom = element.scrollHeight - element.scrollTop === element.clientHeight; - if (isScrollBottom) { $scope.hasNewLogs = false; } diff --git a/static/partials/super-user.html b/static/partials/super-user.html index 944e2c71c..be66343ae 100644 --- a/static/partials/super-user.html +++ b/static/partials/super-user.html @@ -44,7 +44,13 @@
- Please choose a service above to view its logs. + Select a service above to view its local logs + +
+ + Download All Local Logs (.tar.gz) + +
diff --git a/util/systemlogs.py b/util/systemlogs.py new file mode 100644 index 000000000..e2705b2d6 --- /dev/null +++ b/util/systemlogs.py @@ -0,0 +1,15 @@ +import tarfile +import os +import cStringIO + +def build_logs_archive(app): + """ Builds a .tar.gz with the contents of the system logs found for the given app and returns + the binary contents. + """ + path = app.config['SYSTEM_LOGS_PATH'] + buf = cStringIO.StringIO() + + with tarfile.open(mode="w:gz", fileobj=buf) as tar: + tar.add(path, arcname=os.path.basename(path)) + + return buf.getvalue() \ No newline at end of file