blob: 4def64b80d585b6475bd4e74896ef05862a0f0b7 [file] [log] [blame]
# Copyright 2012 The Swarming Authors. All rights reserved.
# Use of this source code is governed by the Apache v2.0 license that can be
# found in the LICENSE file.
"""This module defines Isolate Server frontend url handlers."""
import datetime
import webapp2
from google.appengine.api import users
import acl
import config
import gcs
import handlers_api
import mapreduce_jobs
import stats
import template
from components import auth
from components import stats_framework
from components import stats_framework_gviz
from components import utils
from gviz import gviz_api
# GViz data description.
_GVIZ_DESCRIPTION = {
'failures': ('number', 'Failures'),
'requests': ('number', 'Total'),
'other_requests': ('number', 'Other'),
'uploads': ('number', 'Uploads'),
'uploads_bytes': ('number', 'Uploaded'),
'downloads': ('number', 'Downloads'),
'downloads_bytes': ('number', 'Downloaded'),
'contains_requests': ('number', 'Lookups'),
'contains_lookups': ('number', 'Items looked up'),
}
# Warning: modifying the order here requires updating templates/stats.html.
_GVIZ_COLUMNS_ORDER = (
'key',
'requests',
'other_requests',
'failures',
'uploads',
'downloads',
'contains_requests',
'uploads_bytes',
'downloads_bytes',
'contains_lookups',
)
### Restricted handlers
class RestrictedConfigHandler(auth.AuthenticatingHandler):
@auth.require(auth.is_admin)
def get(self):
self.common(None)
@auth.require(auth.is_admin)
def post(self):
# Convert MultiDict into a dict.
params = {
k: self.request.params.getone(k) for k in self.request.params
if k not in ('keyid', 'xsrf_token')
}
cfg = config.settings(fresh=True)
keyid = int(self.request.get('keyid', '0'))
if cfg.key.integer_id() != keyid:
self.common('Update conflict %s != %s' % (cfg.key.integer_id(), keyid))
return
params['default_expiration'] = int(params['default_expiration'])
cfg.populate(**params)
try:
# Ensure key is correct, it's easy to make a mistake when creating it.
gcs.URLSigner.load_private_key(cfg.gs_private_key)
except Exception as exc:
# TODO(maruel): Handling Exception is too generic. And add self.abort(400)
self.response.write('Bad private key: %s' % exc)
return
cfg.store()
self.common('Settings updated')
def common(self, note):
params = {
'cfg': config.settings(fresh=True),
'note': note,
'path': self.request.path,
'xsrf_token': self.generate_xsrf_token(),
}
self.response.write(
template.render('isolate/restricted_config.html', params))
### Mapreduce related handlers
class RestrictedLaunchMapReduceJob(auth.AuthenticatingHandler):
"""Enqueues a task to start a map reduce job on the backend module.
A tree of map reduce jobs inherits module and version of a handler that
launched it. All UI handlers are executes by 'default' module. So to run a
map reduce on a backend module one needs to pass a request to a task running
on backend module.
"""
@auth.require(auth.is_admin)
def post(self):
job_id = self.request.get('job_id')
assert job_id in mapreduce_jobs.MAPREDUCE_JOBS
# Do not use 'backend' module when running from dev appserver. Mapreduce
# generates URLs that are incompatible with dev appserver URL routing when
# using custom modules.
success = utils.enqueue_task(
url='/internal/taskqueue/mapreduce/launch/%s' % job_id,
queue_name=mapreduce_jobs.MAPREDUCE_TASK_QUEUE,
use_dedicated_module=not utils.is_local_dev_server())
# New tasks should show up on the status page.
if success:
self.redirect('/restricted/mapreduce/status')
else:
self.abort(500, 'Failed to launch the job')
### Non-restricted handlers
class BrowseHandler(auth.AuthenticatingHandler):
@auth.autologin
@auth.require(acl.isolate_readable)
def get(self):
namespace = self.request.get('namespace', 'default')
hash_value = self.request.get('hash', '')
params = {
'hash_value': hash_value,
'namespace': namespace,
'onload': 'update()' if hash_value else '',
}
self.response.write(template.render('isolate/browse.html', params))
class StatsHandler(webapp2.RequestHandler):
"""Returns the statistics web page."""
def get(self):
"""Presents nice recent statistics.
It fetches data from the 'JSON' API.
"""
# Preloads the data to save a complete request.
resolution = self.request.params.get('resolution', 'hours')
duration = utils.get_request_as_int(self.request, 'duration', 120, 1, 1000)
description = _GVIZ_DESCRIPTION.copy()
description.update(stats_framework_gviz.get_description_key(resolution))
table = stats_framework.get_stats(
stats.STATS_HANDLER, resolution, None, duration, True)
params = {
'duration': duration,
'initial_data': gviz_api.DataTable(description, table).ToJSon(
columns_order=_GVIZ_COLUMNS_ORDER),
'now': datetime.datetime.utcnow(),
'resolution': resolution,
}
self.response.write(template.render('isolate/stats.html', params))
class StatsGvizHandlerBase(webapp2.RequestHandler):
RESOLUTION = None
def get(self):
description = _GVIZ_DESCRIPTION.copy()
description.update(
stats_framework_gviz.get_description_key(self.RESOLUTION))
try:
stats_framework_gviz.get_json(
self.request,
self.response,
stats.STATS_HANDLER,
self.RESOLUTION,
description,
_GVIZ_COLUMNS_ORDER)
except ValueError as e:
self.abort(400, str(e))
class StatsGvizDaysHandler(StatsGvizHandlerBase):
RESOLUTION = 'days'
class StatsGvizHoursHandler(StatsGvizHandlerBase):
RESOLUTION = 'hours'
class StatsGvizMinutesHandler(StatsGvizHandlerBase):
RESOLUTION = 'minutes'
### Public pages.
class RootHandler(auth.AuthenticatingHandler):
"""Tells the user to RTM."""
@auth.public
def get(self):
params = {
'is_admin': auth.is_admin(),
'is_user': acl.isolate_readable(),
'mapreduce_jobs': [],
'user_type': acl.get_user_type(),
}
if auth.is_admin():
params['mapreduce_jobs'] = [
{'id': job_id, 'name': job_def['name']}
for job_id, job_def in mapreduce_jobs.MAPREDUCE_JOBS.iteritems()
]
params['xsrf_token'] = self.generate_xsrf_token()
self.response.write(template.render('isolate/root.html', params))
class WarmupHandler(webapp2.RequestHandler):
def get(self):
config.warmup()
auth.warmup()
self.response.headers['Content-Type'] = 'text/plain; charset=utf-8'
self.response.write('ok')
class EmailHandler(webapp2.RequestHandler):
"""Blackhole any email sent."""
def post(self, to):
pass
def get_routes():
return [
# Administrative urls.
webapp2.Route(r'/restricted/config', RestrictedConfigHandler),
# Mapreduce related urls.
webapp2.Route(
r'/restricted/launch_mapreduce',
RestrictedLaunchMapReduceJob),
# User web pages.
webapp2.Route(r'/browse', BrowseHandler),
webapp2.Route(r'/stats', StatsHandler),
webapp2.Route(r'/isolate/api/v1/stats/days', StatsGvizDaysHandler),
webapp2.Route(r'/isolate/api/v1/stats/hours', StatsGvizHoursHandler),
webapp2.Route(r'/isolate/api/v1/stats/minutes', StatsGvizMinutesHandler),
webapp2.Route(r'/', RootHandler),
# AppEngine-specific urls:
webapp2.Route(r'/_ah/mail/<to:.+>', EmailHandler),
webapp2.Route(r'/_ah/warmup', WarmupHandler),
]
def create_application(debug=False):
"""Creates the url router.
The basic layouts is as follow:
- /restricted/.* requires being an instance administrator.
- /content/.* has the public HTTP API.
- /stats/.* has statistics.
"""
acl.bootstrap()
template.bootstrap()
routes = get_routes()
routes.extend(handlers_api.get_routes())
return webapp2.WSGIApplication(routes, debug=debug)