From 5c7a9d0dafc8840ac94b0e746c837a0e444d6489 Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Tue, 23 Dec 2014 11:40:51 -0500 Subject: [PATCH] Add the ability to view the system logs in the superuser endpoint --- endpoints/api/superuser.py | 57 +++++++- static/css/core-ui.css | 164 ++++++++++++++++++++++++ static/css/quay.css | 122 ++---------------- static/directives/cor-log-box.html | 11 ++ static/directives/cor-option.html | 3 + static/directives/cor-options-menu.html | 6 + static/js/app.js | 2 +- static/js/controllers.js | 48 ++++++- static/js/core-ui.js | 92 +++++++++++++ static/partials/super-user.html | 80 +++++++----- 10 files changed, 440 insertions(+), 145 deletions(-) create mode 100644 static/css/core-ui.css create mode 100644 static/directives/cor-log-box.html create mode 100644 static/directives/cor-option.html create mode 100644 static/directives/cor-options-menu.html diff --git a/endpoints/api/superuser.py b/endpoints/api/superuser.py index 753e9caba..17b70a630 100644 --- a/endpoints/api/superuser.py +++ b/endpoints/api/superuser.py @@ -1,9 +1,10 @@ import string import logging import json +import os from random import SystemRandom -from app import app +from app import app, avatar from flask import request from endpoints.api import (ApiResource, nickname, resource, validate_json_request, request_error, @@ -22,6 +23,57 @@ 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))] + + +@resource('/v1/superuser/systemlogs/') +@internal_only +@show_if(features.SUPER_USERS) +class SuperUserGetLogsForService(ApiResource): + """ Resource for fetching the kinds of system logs in the system. """ + @nickname('getSystemLogs') + 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: + abort(404) + + try: + with open(LOGS_PATH % service, 'r') as f: + logs = f.read() + except Exception as ex: + logger.exception('Cannot read logs') + abort(400) + + return { + 'logs': logs + } + + abort(403) + + +@resource('/v1/superuser/systemlogs/') +@internal_only +@show_if(features.SUPER_USERS) +class SuperUserSystemLogServices(ApiResource): + """ Resource for fetching the kinds of system logs in the system. """ + @nickname('listSystemLogServices') + def get(self): + """ List the system logs for the current system. """ + if SuperUserPermission().can(): + return { + 'services': get_immediate_subdirectories(SERVICES_PATH) + } + + abort(403) + + + @resource('/v1/superuser/logs') @internal_only @show_if(features.SUPER_USERS) @@ -33,7 +85,7 @@ class SuperUserLogs(ApiResource): @query_param('endtime', 'Latest time to which to get logs. (%m/%d/%Y %Z)', type=str) @query_param('performer', 'Username for which to filter logs.', type=str) def get(self, args): - """ List the logs for the current system. """ + """ List the usage logs for the current system. """ if SuperUserPermission().can(): performer_name = args['performer'] start_time = args['starttime'] @@ -49,6 +101,7 @@ def user_view(user): 'username': user.username, 'email': user.email, 'verified': user.verified, + 'avatar': avatar.compute_hash(user.email, name=user.username), 'super_user': user.username in app.config['SUPER_USERS'] } diff --git a/static/css/core-ui.css b/static/css/core-ui.css new file mode 100644 index 000000000..d0212e523 --- /dev/null +++ b/static/css/core-ui.css @@ -0,0 +1,164 @@ + +.co-options-menu .fa-gear { + color: #999; + cursor: pointer; +} + +.co-options-menu .dropdown.open .fa-gear { + color: #428BCA; +} + +.co-img-bg-network { + background: url('/static/img/network-tile.png') left top repeat, linear-gradient(30deg, #2277ad, #144768) no-repeat left top fixed; + background-color: #2277ad; + background-size: auto, 100% 100%; +} + +.co-m-navbar { + background-color: white; + margin: 0; + padding-left: 10px; +} + +.co-fx-box-shadow { + -webkit-box-shadow: 0 2px 2px rgba(0, 0, 0, 0.2); + -moz-box-shadow: 0 2px 2px rgba(0, 0, 0, 0.2); + -ms-box-shadow: 0 2px 2px rgba(0, 0, 0, 0.2); + -o-box-shadow: 0 2px 2px rgba(0, 0, 0, 0.2); + box-shadow: 0 2px 2px rgba(0, 0, 0, 0.2); +} + +.co-fx-box-shadow-heavy { + -webkit-box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.4); + -moz-box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.4); + -ms-box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.4); + -o-box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.4); + box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.4); +} + +.co-fx-text-shadow { + text-shadow: rgba(0, 0, 0, 1) 1px 1px 2px; +} + +.co-nav-title { + height: 70px; + margin-top: -22px; +} + +.co-nav-title .co-nav-title-content { + color: white; + text-align: center; +} + +.co-tab-container { + padding: 0px; +} + +.co-tabs { + margin: 0px; + padding: 0px; + width: 82px; + background-color: #e8f1f6; + border-right: 1px solid #DDE7ED; + + display: table-cell; + float: none; + vertical-align: top; +} + +.co-tab-content { + width: 100%; + display: table-cell; + float: none; + padding: 10px; +} + +.co-tabs li { + list-style: none; + display: block; + border-bottom: 1px solid #DDE7ED; +} + + +.co-tabs li.active { + background-color: white; + border-right: 1px solid white; + margin-right: -1px; +} + +.co-tabs li a { + display: block; + width: 82px; + height: 82px; + line-height: 82px; + text-align: center; + font-size: 36px; + color: gray; +} + +.co-tabs li.active a { + color: black; +} + + +.co-main-content-panel { + margin-bottom: 20px; + background-color: #fff; + border: 1px solid transparent; + padding: 10px; + + -webkit-box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.4); + -moz-box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.4); + -ms-box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.4); + -o-box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.4); + box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.4); +} + +.co-tab-panel { + padding: 0px; +} + + +.cor-log-box { + width: 100%; + height: 550px; + position: relative; +} + +.co-log-viewer { + position: absolute; + top: 20px; + left: 20px; + right: 20px; + height: 500px; + + padding: 20px; + + background: rgb(55, 55, 55); + border: 1px solid black; + color: white; + + overflow: scroll; +} + +.co-log-viewer .co-log-content { + font-family: Consolas, "Lucida Console", Monaco, monospace; + font-size: 12px; + white-space: pre; +} + +.cor-log-box .co-log-viewer-new-logs i { + margin-left: 10px; + display: inline-block; +} + +.cor-log-box .co-log-viewer-new-logs { + cursor: pointer; + position: absolute; + bottom: 40px; + right: 30px; + padding: 10px; + color: white; + border-radius: 10px; + background: rgba(72, 158, 72, 0.8); +} \ No newline at end of file diff --git a/static/css/quay.css b/static/css/quay.css index e57d7f872..e2ac8b138 100644 --- a/static/css/quay.css +++ b/static/css/quay.css @@ -88,116 +88,6 @@ margin: 0; } -.co-img-bg-network { - background: url('/static/img/network-tile.png') left top repeat, linear-gradient(30deg, #2277ad, #144768) no-repeat left top fixed; - background-color: #2277ad; - background-size: auto, 100% 100%; -} - -.co-m-navbar { - background-color: white; - margin: 0; - padding-left: 10px; -} - -.co-fx-box-shadow { - -webkit-box-shadow: 0 2px 2px rgba(0, 0, 0, 0.2); - -moz-box-shadow: 0 2px 2px rgba(0, 0, 0, 0.2); - -ms-box-shadow: 0 2px 2px rgba(0, 0, 0, 0.2); - -o-box-shadow: 0 2px 2px rgba(0, 0, 0, 0.2); - box-shadow: 0 2px 2px rgba(0, 0, 0, 0.2); -} - -.co-fx-box-shadow-heavy { - -webkit-box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.4); - -moz-box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.4); - -ms-box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.4); - -o-box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.4); - box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.4); -} - -.co-fx-text-shadow { - text-shadow: rgba(0, 0, 0, 1) 1px 1px 2px; -} - -.co-nav-title { - height: 70px; - margin-top: -22px; -} - -.co-nav-title .co-nav-title-content { - color: white; - text-align: center; -} - -.co-tab-container { - padding: 0px; -} - -.co-tabs { - margin: 0px; - padding: 0px; - width: 82px; - background-color: #e8f1f6; - border-right: 1px solid #DDE7ED; - - display: table-cell; - float: none; - vertical-align: top; -} - -.co-tab-content { - width: 100%; - display: table-cell; - float: none; - padding: 10px; -} - -.co-tabs li { - list-style: none; - display: block; - border-bottom: 1px solid #DDE7ED; -} - - -.co-tabs li.active { - background-color: white; - border-right: 1px solid white; - margin-right: -1px; -} - -.co-tabs li a { - display: block; - width: 82px; - height: 82px; - line-height: 82px; - text-align: center; - font-size: 36px; - color: gray; -} - -.co-tabs li.active a { - color: black; -} - - -.co-main-content-panel { - margin-bottom: 20px; - background-color: #fff; - border: 1px solid transparent; - padding: 10px; - - -webkit-box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.4); - -moz-box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.4); - -ms-box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.4); - -o-box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.4); - box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.4); -} - -.co-tab-panel { - padding: 0px; -} - .main-panel { margin-bottom: 20px; background-color: #fff; @@ -4512,8 +4402,12 @@ pre.command:before { padding: 6px; } -.user-row.super-user td { - background-color: #eeeeee; +.user-row { + border-bottom: 0px; +} + +.user-row td { + vertical-align: middle; } .user-row .user-class { @@ -4982,3 +4876,7 @@ i.slack-icon { #gen-token input[type="checkbox"] { margin-right: 10px; } + +.system-log-download-panel { + padding: 20px; +} diff --git a/static/directives/cor-log-box.html b/static/directives/cor-log-box.html new file mode 100644 index 000000000..c5442d0f7 --- /dev/null +++ b/static/directives/cor-log-box.html @@ -0,0 +1,11 @@ +
+
+
+
+
{{ logs }}
+
+
+
+ New Logs +
+
\ No newline at end of file diff --git a/static/directives/cor-option.html b/static/directives/cor-option.html new file mode 100644 index 000000000..0eb57170b --- /dev/null +++ b/static/directives/cor-option.html @@ -0,0 +1,3 @@ +
  • + +
  • \ No newline at end of file diff --git a/static/directives/cor-options-menu.html b/static/directives/cor-options-menu.html new file mode 100644 index 000000000..8b6cf1e26 --- /dev/null +++ b/static/directives/cor-options-menu.html @@ -0,0 +1,6 @@ + + + \ No newline at end of file diff --git a/static/js/app.js b/static/js/app.js index 0c0c173e7..15ca7a970 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -2225,7 +2225,7 @@ quayApp = angular.module('quay', quayDependencies, function($provide, cfpLoading templateUrl: '/static/partials/repo-list.html', controller: RepoListCtrl, reloadOnSearch: false}). when('/user/', {title: 'Account Settings', description:'Account settings for ' + title, templateUrl: '/static/partials/user-admin.html', reloadOnSearch: false, controller: UserAdminCtrl}). - when('/superuser/', {title: 'Superuser Admin Panel', description:'Admin panel for ' + title, templateUrl: '/static/partials/super-user.html', + when('/superuser/', {title: 'Enterprise Registry Setup', description:'Admin panel for ' + title, templateUrl: '/static/partials/super-user.html', reloadOnSearch: false, controller: SuperUserAdminCtrl, newLayout: true}). when('/guide/', {title: 'Guide', description:'Guide to using private docker repositories on ' + title, templateUrl: '/static/partials/guide.html', diff --git a/static/js/controllers.js b/static/js/controllers.js index b8f6ccb11..9abbec906 100644 --- a/static/js/controllers.js +++ b/static/js/controllers.js @@ -2810,7 +2810,7 @@ function ManageApplicationCtrl($scope, $routeParams, $rootScope, $location, $tim } -function SuperUserAdminCtrl($scope, ApiService, Features, UserService) { +function SuperUserAdminCtrl($scope, $timeout, ApiService, Features, UserService, AngularPollChannel) { if (!Features.SUPER_USERS) { return; } @@ -2822,6 +2822,52 @@ function SuperUserAdminCtrl($scope, ApiService, Features, UserService) { $scope.newUser = {}; $scope.createdUsers = []; $scope.systemUsage = null; + $scope.debugServices = null; + $scope.debugLogs = null; + $scope.pollChannel = null; + $scope.logsScrolled = false; + + $scope.viewSystemLogs = function(service) { + if ($scope.pollChannel) { + $scope.pollChannel.stop(); + } + + $scope.debugService = service; + $scope.debugLogs = null; + + $scope.pollChannel = AngularPollChannel.create($scope, $scope.loadServiceLogs, 1 * 1000 /* 1s */); + $scope.pollChannel.start(); + }; + + $scope.loadServiceLogs = function(callback) { + if (!$scope.debugService) { return; } + + var params = { + 'service': $scope.debugService + }; + + var errorHandler = ApiService.errorDisplay('Cannot load system logs. Please contact support.', + function() { + callback(false); + }) + + ApiService.getSystemLogs(null, params, /* background */true).then(function(resp) { + $scope.debugLogs = resp['logs']; + callback(true); + }, errorHandler); + }; + + $scope.loadDebugServices = function() { + if ($scope.pollChannel) { + $scope.pollChannel.stop(); + } + + $scope.debugService = null; + + ApiService.listSystemLogServices().then(function(resp) { + $scope.debugServices = resp['services']; + }, ApiService.errorDisplay('Cannot load system logs. Please contact support.')) + }; $scope.getUsage = function() { if ($scope.systemUsage) { return; } diff --git a/static/js/core-ui.js b/static/js/core-ui.js index 64ffc6f68..f9349f855 100644 --- a/static/js/core-ui.js +++ b/static/js/core-ui.js @@ -1,4 +1,96 @@ angular.module("core-ui", []) + .directive('corLogBox', function() { + var directiveDefinitionObject = { + priority: 1, + templateUrl: '/static/directives/cor-log-box.html', + replace: true, + transclude: true, + restrict: 'C', + scope: { + 'logs': '=logs' + }, + controller: function($rootScope, $scope, $element, $timeout) { + $scope.hasNewLogs = false; + + var scrollHandlerBound = false; + var isAnimatedScrolling = false; + var isScrollBottom = true; + + var scrollHandler = function() { + if (isAnimatedScrolling) { return; } + var element = $element.find("#co-log-viewer")[0]; + isScrollBottom = element.scrollHeight - element.scrollTop === element.clientHeight; + + if (isScrollBottom) { + $scope.hasNewLogs = false; + } + }; + + var animateComplete = function() { + isAnimatedScrolling = false; + }; + + $scope.moveToBottom = function() { + $scope.hasNewLogs = false; + isAnimatedScrolling = true; + isScrollBottom = true; + + $element.find("#co-log-viewer").animate( + { scrollTop: $element.find("#co-log-content").height() }, "slow", null, animateComplete); + }; + + $scope.$watch('logs', function(value, oldValue) { + if (!value) { return; } + + $timeout(function() { + if (!scrollHandlerBound) { + $element.find("#co-log-viewer").on('scroll', scrollHandler); + scrollHandlerBound = true; + } + + if (!isScrollBottom) { + $scope.hasNewLogs = true; + return; + } + + $scope.moveToBottom(); + }, 500); + }); + } + }; + return directiveDefinitionObject; + }) + + .directive('corOptionsMenu', function() { + var directiveDefinitionObject = { + priority: 1, + templateUrl: '/static/directives/cor-options-menu.html', + replace: true, + transclude: true, + restrict: 'C', + scope: {}, + controller: function($rootScope, $scope, $element) { + } + }; + return directiveDefinitionObject; + }) + + .directive('corOption', function() { + var directiveDefinitionObject = { + priority: 1, + templateUrl: '/static/directives/cor-option.html', + replace: true, + transclude: true, + restrict: 'C', + scope: { + 'optionClick': '&optionClick' + }, + controller: function($rootScope, $scope, $element) { + } + }; + return directiveDefinitionObject; + }) + .directive('corTitle', function() { var directiveDefinitionObject = { diff --git a/static/partials/super-user.html b/static/partials/super-user.html index 6a49e6eed..944e2c71c 100644 --- a/static/partials/super-user.html +++ b/static/partials/super-user.html @@ -6,10 +6,10 @@
    - + @@ -19,12 +19,37 @@ - +
    + +
    + setup +
    + + +
    +
    + +
    + + + +
    + Please choose a service above to view its logs. +
    +
    +
    +
    +
    @@ -34,7 +59,7 @@
    + current="systemUsage.usage" usage-title="Deployed Containers">
    @@ -51,10 +76,12 @@ You are nearing the number of allowed deployed repositories. It might be time to think about upgrading your subscription by contacting CoreOS Sales.
    + + For more information: See Here.
    -
    +
    {{ usersError }} @@ -72,42 +99,37 @@ + + class="user-row"> +
    Username E-mail address
    + + - {{ current_user.username }} {{ current_user.email }} - - + + + Change Password + + + Send Recovery Email + + + Delete User + +