Add the ability to view the system logs in the superuser endpoint

This commit is contained in:
Joseph Schorr 2014-12-23 11:40:51 -05:00
parent 1f9f4ef26b
commit 5c7a9d0daf
10 changed files with 440 additions and 145 deletions

View file

@ -1,9 +1,10 @@
import string import string
import logging import logging
import json import json
import os
from random import SystemRandom from random import SystemRandom
from app import app from app import app, avatar
from flask import request from flask import request
from endpoints.api import (ApiResource, nickname, resource, validate_json_request, request_error, from endpoints.api import (ApiResource, nickname, resource, validate_json_request, request_error,
@ -22,6 +23,57 @@ import features
logger = logging.getLogger(__name__) 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/<service>')
@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') @resource('/v1/superuser/logs')
@internal_only @internal_only
@show_if(features.SUPER_USERS) @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('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) @query_param('performer', 'Username for which to filter logs.', type=str)
def get(self, args): def get(self, args):
""" List the logs for the current system. """ """ List the usage logs for the current system. """
if SuperUserPermission().can(): if SuperUserPermission().can():
performer_name = args['performer'] performer_name = args['performer']
start_time = args['starttime'] start_time = args['starttime']
@ -49,6 +101,7 @@ def user_view(user):
'username': user.username, 'username': user.username,
'email': user.email, 'email': user.email,
'verified': user.verified, 'verified': user.verified,
'avatar': avatar.compute_hash(user.email, name=user.username),
'super_user': user.username in app.config['SUPER_USERS'] 'super_user': user.username in app.config['SUPER_USERS']
} }

164
static/css/core-ui.css Normal file
View file

@ -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);
}

View file

@ -88,116 +88,6 @@
margin: 0; 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 { .main-panel {
margin-bottom: 20px; margin-bottom: 20px;
background-color: #fff; background-color: #fff;
@ -4512,8 +4402,12 @@ pre.command:before {
padding: 6px; padding: 6px;
} }
.user-row.super-user td { .user-row {
background-color: #eeeeee; border-bottom: 0px;
}
.user-row td {
vertical-align: middle;
} }
.user-row .user-class { .user-row .user-class {
@ -4982,3 +4876,7 @@ i.slack-icon {
#gen-token input[type="checkbox"] { #gen-token input[type="checkbox"] {
margin-right: 10px; margin-right: 10px;
} }
.system-log-download-panel {
padding: 20px;
}

View file

@ -0,0 +1,11 @@
<div class="co-log-box-element">
<div id="co-log-viewer" class="co-log-viewer" ng-if="logs">
<div class="quay-spinner" ng-if="!logs"></div>
<div class="co-log-container">
<div id="co-log-content" class="co-log-content">{{ logs }}</div>
</div>
</div>
<div class="co-log-viewer-new-logs" ng-show="hasNewLogs" ng-click="moveToBottom()">
New Logs <i class="fa fa-lg fa-arrow-circle-down"></i>
</div>
</div>

View file

@ -0,0 +1,3 @@
<li>
<a href="javascript:void(0)" ng-click="optionClick()" ng-transclude></a>
</li>

View file

@ -0,0 +1,6 @@
<span class="co-options-menu">
<div class="dropdown" style="text-align: left;">
<i class="fa fa-gear fa-lg dropdown-toggle" data-toggle="dropdown" data-title="Options" bs-tooltip></i>
<ul class="dropdown-menu pull-right" ng-transclude></ul>
</div>
</span>

View file

@ -2225,7 +2225,7 @@ quayApp = angular.module('quay', quayDependencies, function($provide, cfpLoading
templateUrl: '/static/partials/repo-list.html', controller: RepoListCtrl, reloadOnSearch: false}). 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', when('/user/', {title: 'Account Settings', description:'Account settings for ' + title, templateUrl: '/static/partials/user-admin.html',
reloadOnSearch: false, controller: UserAdminCtrl}). 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}). reloadOnSearch: false, controller: SuperUserAdminCtrl, newLayout: true}).
when('/guide/', {title: 'Guide', description:'Guide to using private docker repositories on ' + title, when('/guide/', {title: 'Guide', description:'Guide to using private docker repositories on ' + title,
templateUrl: '/static/partials/guide.html', templateUrl: '/static/partials/guide.html',

View file

@ -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) { if (!Features.SUPER_USERS) {
return; return;
} }
@ -2822,6 +2822,52 @@ function SuperUserAdminCtrl($scope, ApiService, Features, UserService) {
$scope.newUser = {}; $scope.newUser = {};
$scope.createdUsers = []; $scope.createdUsers = [];
$scope.systemUsage = null; $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() { $scope.getUsage = function() {
if ($scope.systemUsage) { return; } if ($scope.systemUsage) { return; }

View file

@ -1,4 +1,96 @@
angular.module("core-ui", []) 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() { .directive('corTitle', function() {
var directiveDefinitionObject = { var directiveDefinitionObject = {

View file

@ -6,10 +6,10 @@
<div class="cor-tab-panel"> <div class="cor-tab-panel">
<div class="cor-tabs"> <div class="cor-tabs">
<!--<span class="cor-tab" tab-active="true" tab-title="Registry Settings" tab-target="#setup" <span class="cor-tab" tab-active="true" tab-title="Registry Settings" tab-target="#setup"
tab-init="loadConfig()"> tab-init="loadConfig()">
<i class="fa fa-cog"></i> <i class="fa fa-cog"></i>
</span>--> </span>
<span class="cor-tab" tab-title="Manage Users" tab-target="#users" tab-init="loadUsers()"> <span class="cor-tab" tab-title="Manage Users" tab-target="#users" tab-init="loadUsers()">
<i class="fa fa-group"></i> <i class="fa fa-group"></i>
</span> </span>
@ -19,12 +19,37 @@
<span class="cor-tab" tab-title="Usage Logs" tab-target="#logs" tab-init="loadUsageLogs()"> <span class="cor-tab" tab-title="Usage Logs" tab-target="#logs" tab-init="loadUsageLogs()">
<i class="fa fa-bar-chart"></i> <i class="fa fa-bar-chart"></i>
</span> </span>
<!--<span class="cor-tab" tab-title="Internal Logs and Debugging" tab-target="#debug"> <span class="cor-tab" tab-title="Internal Logs and Debugging" tab-target="#debug" tab-init="loadDebugServices()">
<i class="fa fa-bug"></i> <i class="fa fa-bug"></i>
</span>--> </span>
</div> <!-- /cor-tabs --> </div> <!-- /cor-tabs -->
<div class="cor-tab-content"> <div class="cor-tab-content">
<!-- Setup tab -->
<div id="setup" class="tab-pane active">
setup
</div>
<!-- Debugging tab -->
<div id="debug" class="tab-pane">
<div class="quay-spinner" ng-show="!debugServices"></div>
<div role="tabpanel" ng-show="debugServices">
<!-- Nav tabs -->
<ul class="nav nav-tabs" role="tablist">
<li role="presentation" ng-repeat="service in debugServices"
ng-class="debugService == service ? 'active' : ''">
<a href="javascript:void(0)" ng-click="viewSystemLogs(service)">{{ service }}</a>
</li>
</ul>
<div class="system-log-download-panel" ng-if="!debugService">
Please choose a service above to view its logs.
</div>
<div class="cor-log-box" logs="debugLogs" ng-show="debugService"></div>
</div>
</div>
<!-- Logs tab --> <!-- Logs tab -->
<div id="logs" class="tab-pane"> <div id="logs" class="tab-pane">
<div class="logsView" makevisible="logsCounter" all-logs="true"></div> <div class="logsView" makevisible="logsCounter" all-logs="true"></div>
@ -34,7 +59,7 @@
<div id="usage-counter" class="tab-pane"> <div id="usage-counter" class="tab-pane">
<div class="quay-spinner" ng-show="systemUsage == null"></div> <div class="quay-spinner" ng-show="systemUsage == null"></div>
<div class="usage-chart" total="systemUsage.allowed" limit="systemUsageLimit" <div class="usage-chart" total="systemUsage.allowed" limit="systemUsageLimit"
current="systemUsage.usage" usage-title="Deployed Repositories"></div> current="systemUsage.usage" usage-title="Deployed Containers"></div>
<!-- Alerts --> <!-- Alerts -->
<div class="alert alert-danger" ng-show="systemUsageLimit == 'over' && systemUsage"> <div class="alert alert-danger" ng-show="systemUsageLimit == 'over' && systemUsage">
@ -51,10 +76,12 @@
You are nearing the number of allowed deployed repositories. It might be time to think about You are nearing the number of allowed deployed repositories. It might be time to think about
upgrading your subscription by contacting <a href="mailto:sales@coreos.com">CoreOS Sales</a>. upgrading your subscription by contacting <a href="mailto:sales@coreos.com">CoreOS Sales</a>.
</div> </div>
For more information: <a href="https://coreos.com/products/enterprise-registry/plans/">See Here</a>.
</div> <!-- /usage-counter tab--> </div> <!-- /usage-counter tab-->
<!-- Users tab --> <!-- Users tab -->
<div id="users" class="tab-pane active"> <div id="users" class="tab-pane">
<div class="quay-spinner" ng-show="!users"></div> <div class="quay-spinner" ng-show="!users"></div>
<div class="alert alert-error" ng-show="usersError"> <div class="alert alert-error" ng-show="usersError">
{{ usersError }} {{ usersError }}
@ -72,42 +99,37 @@
<table class="table"> <table class="table">
<thead> <thead>
<th style="width: 24px;"></th>
<th>Username</th> <th>Username</th>
<th>E-mail address</th> <th>E-mail address</th>
<th style="width: 24px;"></th> <th style="width: 24px;"></th>
</thead> </thead>
<tr ng-repeat="current_user in (users | filter:search | orderBy:'username' | limitTo:100)" <tr ng-repeat="current_user in (users | filter:search | orderBy:'username' | limitTo:100)"
class="user-row" class="user-row">
ng-class="current_user.super_user ? 'super-user' : ''"> <td>
<span class="avatar" hash="current_user.avatar" size="24"></span>
</td>
<td> <td>
<i class="fa fa-user" style="margin-right: 6px"></i>
{{ current_user.username }} {{ current_user.username }}
</td> </td>
<td> <td>
<a href="mailto:{{ current_user.email }}">{{ current_user.email }}</a> <a href="mailto:{{ current_user.email }}">{{ current_user.email }}</a>
</td> </td>
<td style="text-align: center;"> <td style="text-align: center;">
<i class="fa fa-ge fa-lg" ng-if="current_user.super_user" data-title="Super User" bs-tooltip></i> <span class="cor-options-menu"
<div class="dropdown" style="text-align: left;"
ng-if="user.username != current_user.username && !current_user.super_user"> ng-if="user.username != current_user.username && !current_user.super_user">
<button class="btn btn-default dropdown-toggle" data-toggle="dropdown"> <span class="cor-option" option-click="showChangePassword(current_user)">
<i class="caret"></i>
</button>
<ul class="dropdown-menu pull-right">
<li>
<a href="javascript:void(0)" ng-click="showChangePassword(current_user)">
<i class="fa fa-key"></i> Change Password <i class="fa fa-key"></i> Change Password
</a> </span>
<a href="javascript:void(0)" ng-click="sendRecoveryEmail(current_user)" quay-show="Features.MAILING"> <span class="cor-option" option-click="sendRecoveryEmail(current_user)"
quay-show="Features.MAILING">
<i class="fa fa-envelope"></i> Send Recovery Email <i class="fa fa-envelope"></i> Send Recovery Email
</a> </span>
<a href="javascript:void(0)" ng-click="showDeleteUser(current_user)"> <span class="cor-option" option-click="showDeleteUser(current_user)">
<i class="fa fa-times"></i> Delete User <i class="fa fa-times"></i> Delete User
</a> </span>
</li> </span>
</ul>
</div>
</td> </td>
</tr> </tr>
</table> </table>