web: add /tenants route

This change adds the zuul:tenant_list to the scheduler gearman worker
to expose the list of tenants.

This change also adds the /tenants.json endpoint to the zuul-web as well as
a web interface to list tenants at / or /tenants.html.

Change-Id: Ia8edb52ec97ebe53205427c828944116eebe03b7
This commit is contained in:
Tristan Cacqueray 2017-09-12 20:47:19 +00:00
parent d646c12778
commit e290f893a7
6 changed files with 227 additions and 0 deletions

View File

@ -56,6 +56,7 @@ class RPCListener(object):
self.worker.registerFunction("zuul:promote")
self.worker.registerFunction("zuul:get_running_jobs")
self.worker.registerFunction("zuul:get_job_log_stream_address")
self.worker.registerFunction("zuul:tenant_list")
def getFunctions(self):
functions = {}
@ -269,3 +270,10 @@ class RPCListener(object):
job_log_stream_address['server'] = build.worker.hostname
job_log_stream_address['port'] = build.worker.log_port
job.sendWorkComplete(json.dumps(job_log_stream_address))
def handle_tenant_list(self, job):
output = []
for tenant_name, tenant in self.sched.abide.tenants.items():
output.append({'name': tenant_name,
'projects': len(tenant.untrusted_projects)})
job.sendWorkComplete(json.dumps(output))

View File

@ -148,6 +148,31 @@ class LogStreamingHandler(object):
return ws
class GearmanHandler(object):
log = logging.getLogger("zuul.web.GearmanHandler")
def __init__(self, rpc):
self.rpc = rpc
self.controllers = {
'tenant_list': self.tenant_list,
}
def tenant_list(self, request):
job = self.rpc.submitJob('zuul:tenant_list', {})
return web.json_response(json.loads(job.data[0]))
async def processRequest(self, request, action):
try:
resp = self.controllers[action](request)
except asyncio.CancelledError:
self.log.debug("request handling cancelled")
except Exception as e:
self.log.exception("exception:")
resp = web.json_response({'error_description': 'Internal error'},
status=500)
return resp
class ZuulWeb(object):
log = logging.getLogger("zuul.web.ZuulWeb")
@ -163,11 +188,22 @@ class ZuulWeb(object):
self.rpc = zuul.rpcclient.RPCClient(gear_server, gear_port,
ssl_key, ssl_cert, ssl_ca)
self.log_streaming_handler = LogStreamingHandler(self.rpc)
self.gearman_handler = GearmanHandler(self.rpc)
async def _handleWebsocket(self, request):
return await self.log_streaming_handler.processRequest(
request)
async def _handleTenantsRequest(self, request):
return await self.gearman_handler.processRequest(request,
'tenant_list')
async def _handleStaticRequest(self, request):
fp = None
if request.path.endswith("tenants.html") or request.path.endswith("/"):
fp = os.path.join(STATIC_DIR, "index.html")
return web.FileResponse(fp)
def run(self, loop=None):
"""
Run the websocket daemon.
@ -181,6 +217,9 @@ class ZuulWeb(object):
"""
routes = [
('GET', '/console-stream', self._handleWebsocket),
('GET', '/tenants.json', self._handleTenantsRequest),
('GET', '/tenants.html', self._handleStaticRequest),
('GET', '/', self._handleStaticRequest),
]
self.log.debug("ZuulWeb starting")

33
zuul/web/static/README Normal file
View File

@ -0,0 +1,33 @@
External requirements needs to be installed in these locations
* /static/js/angular.min.js
* /static/js/jquery.min.js
* /static/js/jquery-visibility.min.js
* /static/js/jquery.graphite.min.js
* /static/bootstrap/css/bootstrap.min.css
Here is an example apache vhost configuration:
<VirtualHost zuul-web.example.com:80>
DocumentRoot /var/www/zuul-web
LogLevel warn
Alias "/static" "/var/www/zuul-web"
AliasMatch "^/.*/(.*).html" "/var/www/zuul-web/$1.html"
AliasMatch "^/.*.html" "/var/www/zuul-web/index.html"
<Directory /var/www/zuul-web>
Require all granted
Order allow,deny
Allow from all
</Directory>
# Console-stream needs a special proxy-pass for websocket
ProxyPass /console-stream ws://localhost:9000/console-stream nocanon retry=0
ProxyPassReverse /console-stream ws://localhost:9000/console-stream
# Then only the json calls are sent to the zuul-web endpoints
ProxyPassMatch ^/(.*.json)$ http://localhost:9000/$1 nocanon retry=0
ProxyPassReverse / http://localhost:9000/
</VirtualHost>
Then copy the zuul/web/static/ files and external requirements to
/var/www/zuul-web

View File

@ -0,0 +1,57 @@
<!--
Copyright 2017 Red Hat
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.
-->
<!DOCTYPE html>
<html>
<head>
<title>Zuul Tenants</title>
<link rel="stylesheet" href="/static/bootstrap/css/bootstrap.min.css">
<link rel="stylesheet" href="static/styles/zuul.css" />
<script src="/static/js/jquery.min.js"></script>
<script src="/static/js/angular.min.js"></script>
<script src="static/javascripts/zuul.angular.js"></script>
</head>
<body ng-app="zuulTenants" ng-controller="mainController"><div class="container-fluid">
<nav class="navbar navbar-default">
<div class="container-fluid">
<div class="navbar-header">
<a class="navbar-brand">Zuul Dashboard</a>
</div>
<ul class="nav navbar-nav">
<li class="active"><a href="tenants.html">Tenants</a></li>
</ul>
</div>
</nav>
<table class="table table-hover table-condensed">
<thead>
<tr>
<th>Name</th>
<th>Status</th>
<th>Jobs</th>
<th>Builds</th>
<th>Projects count</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="tenant in tenants">
<td>{{ tenant.name }}</td>
<td><a href="{{ tenant.name }}/status.html">status</a></td>
<td><a href="{{ tenant.name }}/jobs.html">jobs</a></td>
<td><a href="{{ tenant.name }}/builds.html">builds</a></td>
<td>{{ tenant.projects }}</td>
</tr>
</tbody>
</table>
</div></body></html>

View File

@ -0,0 +1,32 @@
// @licstart The following is the entire license notice for the
// JavaScript code in this page.
//
// Copyright 2017 Red Hat
//
// 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.
//
// @licend The above is the entire license notice
// for the JavaScript code in this page.
angular.module('zuulTenants', []).controller(
'mainController', function($scope, $http)
{
$scope.tenants = undefined;
$scope.tenants_fetch = function() {
$http.get("tenants.json")
.then(function success(result) {
$scope.tenants = result.data;
});
}
$scope.tenants_fetch();
});

View File

@ -0,0 +1,58 @@
.zuul-change {
margin-bottom: 10px;
}
.zuul-change-id {
float: right;
}
.zuul-job-result {
float: right;
width: 70px;
height: 15px;
margin: 2px 0 0 0;
}
.zuul-change-total-result {
height: 10px;
width: 100px;
margin: 0;
display: inline-block;
vertical-align: middle;
}
.zuul-spinner,
.zuul-spinner:hover {
opacity: 0;
transition: opacity 0.5s ease-out;
cursor: default;
pointer-events: none;
}
.zuul-spinner-on,
.zuul-spinner-on:hover {
opacity: 1;
transition-duration: 0.2s;
cursor: progress;
}
.zuul-change-cell {
padding-left: 5px;
}
.zuul-change-job {
padding: 2px 8px;
}
.zuul-job-name {
font-size: small;
}
.zuul-non-voting-desc {
font-size: smaller;
}
.zuul-patchset-header {
font-size: small;
padding: 8px 12px;
}