"""
The website module uses `Tornado <http://www.tornadoweb.org>`_,
a fast and scalable python web server.
There are three groups of handlers:
* Main website
This is the external website users will see when interacting with IceProd.
It has been broken down into several sub-handlers for easier maintenance.
* JSONRPC
This is the machine-readable portion of the website. Jobs talk to the
server through this mechanism. The main website also uses this for
various actions.
* lib downloads
This is a directory of downloads local to the IceProd instance.
Usually this will be the necessary software to run the iceprod core.
"""
from __future__ import absolute_import, division, print_function
import sys
import os
import time
import random
import binascii
import socket
from threading import Thread,Event,Condition
import logging
from contextlib import contextmanager
from functools import partial, wraps
try:
from urlparse import urlparse
except ImportError:
from urllib.parse import urlparse
from datetime import timedelta
# override tornado json encoder and decoder so we can use dataclasses objects
import iceprod.core.jsonUtil
import tornado.escape
tornado.escape.json_encode = iceprod.core.jsonUtil.json_encode
tornado.escape.json_decode = iceprod.core.jsonUtil.json_decode
import tornado.ioloop
import tornado.web
import tornado.httpserver
import tornado.gen
import tornado.concurrent
import concurrent.futures
import iceprod
from iceprod.server import GlobalID, get_pkgdata_filename
from iceprod.server import module
from iceprod.server.nginx import Nginx, find_nginx
from iceprod.server.ssl_cert import create_cert, verify_cert
from iceprod.server.file_io import AsyncFileIO
from iceprod.server.modules.db import DBAPI
import iceprod.core.functions
from iceprod.server import documentation
logger = logging.getLogger('website')
[docs]class website(module.module):
"""
The main website module.
Run the website, which is required for anything to work.
"""
def __init__(self,*args,**kwargs):
# run default init
super(website,self).__init__(*args,**kwargs)
self.service['logrotate'] = self.logrotate
# set up local variables
self.nginx = None
self.http_server = None
[docs] def stop(self):
"""Stop website"""
# stop nginx
try:
if self.nginx:
self.nginx.stop()
except Exception:
logger.error('cannot stop Nginx', exc_info=True)
# stop tornado
try:
if self.http_server:
self.http_server.stop()
except Exception:
logger.error('cannot stop tornado', exc_info=True)
super(website,self).stop()
[docs] def kill(self):
"""Kill website"""
# kill nginx
try:
if self.nginx:
self.nginx.kill()
except Exception:
logger.error('cannot kill Nginx', exc_info=True)
super(website,self).kill()
[docs] def logrotate(self):
"""Rotate the Nginx logs."""
logger.warning('got a logrotate() call')
try:
if self.nginx:
# rotate nginx logs
self.nginx.logrotate()
# tornado uses regular python logs, which rotate automatically
except Exception:
logger.warning('error in logrotate', exc_info=True)
[docs] def start(self):
"""Run the website"""
super(website,self).start()
try:
# make sure directories are set up properly
for d in self.cfg['webserver']:
if '_dir' in d:
path = self.cfg['webserver'][d]
path = os.path.expanduser(os.path.expandvars(path))
try:
os.makedirs(path)
except Exception:
pass
# get package data
static_path = get_pkgdata_filename('iceprod.server','data/www')
if static_path is None or not os.path.exists(static_path):
logger.info('static path: %r',static_path)
raise Exception('bad static path')
template_path = get_pkgdata_filename('iceprod.server','data/www_templates')
if template_path is None or not os.path.exists(template_path):
logger.info('template path: %r',template_path)
raise Exception('bad template path')
# detect nginx
try:
if ('nginx' in self.cfg['webserver'] and
not self.cfg['webserver']['nginx']):
raise Exception('nginx explicitly disabled')
find_nginx()
except Exception:
if ('system' in self.cfg and 'ssl' in self.cfg['system']
and self.cfg['system']['ssl'] is not False):
logger.error('Nginx not present when SSL requested')
raise
logger.error('Nginx not present, running Tornado directly')
logger.error('(Note that this mode is not secure)')
self.nginx = None
else:
# configure nginx
kwargs = {}
if (self.cfg and 'webserver' in self.cfg and
'request_timeout' in self.cfg['webserver']):
try:
timeout = int(self.cfg['webserver']['request_timeout'])
except Exception:
pass
else:
kwargs['request_timeout'] = timeout
if ('download' in self.cfg and 'http_username' in self.cfg['download']
and self.cfg['download']['http_username']):
kwargs['username'] = self.cfg['download']['http_username']
if ('download' in self.cfg and 'http_password' in self.cfg['download']
and self.cfg['download']['http_password']):
kwargs['password'] = self.cfg['download']['http_password']
if ('system' in self.cfg and 'ssl' in self.cfg['system']
and self.cfg['system']['ssl'] is not False):
cert = None
key = None
if ('autogen' in self.cfg['system']['ssl']
and self.cfg['system']['ssl']['autogen']):
if (os.path.exists(self.cfg['system']['ssl']['cert'])
and os.path.exists(self.cfg['system']['ssl']['key'])):
cert = self.cfg['system']['ssl']['cert']
key = self.cfg['system']['ssl']['key']
if not verify_cert(cert,key):
cert = None
key = None
elif ('cert' in self.cfg['system']['ssl']
and 'key' in self.cfg['system']['ssl']):
if (os.path.exists(self.cfg['system']['ssl']['cert'])
and os.path.exists(self.cfg['system']['ssl']['key'])):
cert = self.cfg['system']['ssl']['cert']
key = self.cfg['system']['ssl']['key']
else:
raise Exception('Bad ssl cert or key')
if not cert:
# auto-generate self-signed cert
create_cert('$PWD/cert','$PWD/key',days=365)
cert = os.path.expandvars('$PWD/cert')
key = os.path.expandvars('$PWD/key')
self.cfg['system']['ssl']['autogen'] = True
self.cfg['system']['ssl']['cert'] = cert
self.cfg['system']['ssl']['key'] = key
kwargs['sslcert'] = cert
kwargs['sslkey'] = key
if 'cacert' in self.cfg['system']['ssl']:
if not os.path.exists(self.cfg['system']['ssl']['cacert']):
raise Exception('Bad path to cacert')
kwargs['cacert'] = self.cfg['system']['ssl']['cacert']
kwargs['port'] = self.cfg['webserver']['port']
kwargs['proxy_port'] = self.cfg['webserver']['tornado_port']
kwargs['static_dir'] = static_path
# start nginx
try:
self.nginx = Nginx(**kwargs)
self.nginx.start()
except Exception:
logger.critical('cannot start Nginx:',exc_info=True)
raise
# configure tornado
def tornado_logger(handler):
if handler.get_status() < 400:
log_method = logger.debug
elif handler.get_status() < 500:
log_method = logger.warning
else:
log_method = logger.error
request_time = 1000.0 * handler.request.request_time()
log_method("%d %s %.2fms", handler.get_status(),
handler._request_summary(), request_time)
#UploadHandler.upload_prefix = '/upload'
handler_args = {
'cfg':self.cfg,
'modules':self.modules,
'fileio':AsyncFileIO(executor=self.executor),
'statsd':self.statsd,
}
if 'debug' in self.cfg['webserver'] and self.cfg['webserver']['debug']:
handler_args['debug'] = True
lib_args = handler_args.copy()
lib_args['prefix'] = '/lib/'
lib_args['directory'] = os.path.expanduser(os.path.expandvars(
self.cfg['webserver']['lib_dir']))
if 'cookie_secret' in self.cfg['webserver']:
cookie_secret = self.cfg['webserver']['cookie_secret']
else:
cookie_secret = ''.join(hex(random.randint(0,15))[-1] for _ in range(64))
self.cfg['webserver']['cookie_secret'] = cookie_secret
self.application = tornado.web.Application([
(r"/jsonrpc", JSONRPCHandler, handler_args),
(r"/lib/.*", LibHandler, lib_args),
(r"/", Default, handler_args),
(r"/submit", Submit, handler_args),
(r"/config", Config, handler_args),
(r"/dataset(/.*)?", Dataset, handler_args),
(r"/task(/.*)?", Task, handler_args),
(r"/job(/.*)?", Job, handler_args),
(r"/site(/.*)?", Site, handler_args),
(r"/help", Help, handler_args),
(r"/docs/(.*)", Documentation, handler_args),
(r"/log/(.*)/(.*)", Log, handler_args),
(r"/groups", GroupsHandler, handler_args),
(r"/login", Login, handler_args),
(r"/logout", Logout, handler_args),
(r"/.*", Other, handler_args),
],static_path=static_path,
template_path=template_path,
log_function=tornado_logger,
xsrf_cookies=True,
cookie_secret=binascii.unhexlify(cookie_secret),
login_url='/login')
self.http_server = tornado.httpserver.HTTPServer(
self.application,
xheaders=True)
# start tornado
if self.nginx is None:
tornado_port = self.cfg['webserver']['port']
tornado_address = '0.0.0.0' # bind to all
else:
tornado_port = self.cfg['webserver']['tornado_port']
tornado_address = 'localhost' # bind locally
logger.warning('tornado bound to port %d', tornado_port)
self.http_server.bind(tornado_port, address=tornado_address, family=socket.AF_INET)
self.http_server.start()
logger.warning('tornado starting')
except Exception:
logger.error('website startup error',exc_info=True)
raise
[docs]def catch_error(method):
"""Decorator to catch and handle errors on handlers"""
@wraps(method)
def wrapper(self, *args, **kwargs):
try:
return method(self, *args, **kwargs)
except Exception as e:
self.statsd.incr(self.__class__.__name__+'.error')
logger.warning('Error in website handler', exc_info=True)
message = 'Error generating page for '+self.__class__.__name__
if self.debug:
message = message + '\n' + str(e)
self.send_error(500, message=message)
return wrapper
[docs]def authenticated_secure(method):
"""Decorate methods with this to require that the user be logged in
to a secure area.
If the user is not logged in, they will be redirected to the configured
`login url <RequestHandler.get_login_url>`.
If you configure a login url with a query parameter, Tornado will
assume you know what you're doing and use it as-is. If not, it
will add a `next` parameter so the login page knows where to send
you once you're logged in.
"""
@wraps(method)
def wrapper(self, *args, **kwargs):
if not self.current_user_secure:
if self.request.method in ("GET", "HEAD"):
url = self.get_login_url()
if "?" not in url:
if urlparse.urlsplit(url).scheme:
# if login url is absolute, make next absolute too
next_url = self.request.full_url()
else:
next_url = self.request.uri
url += "?" + urlencode(dict(next=next_url,secure=True))
self.redirect(url)
return
raise HTTPError(403)
return method(self, *args, **kwargs)
return wrapper
[docs]class MyHandler(tornado.web.RequestHandler):
"""Default Handler"""
[docs] def initialize(self, cfg, modules, fileio, debug=False, statsd=None):
"""
Get some params from the website module
:param cfg: the global config
:param modules: modules handle
:param fileio: AsyncFileIO object
:param debug: debug flag (optional)
"""
self.cfg = cfg
self.modules = modules
self.fileio = fileio
self.debug = debug
self.statsd = statsd
@tornado.gen.coroutine
[docs] def db_call(self,func_name,*args,**kwargs):
"""Make a database call, returning the result"""
logger.info('db_call for %s',func_name)
try:
f = self.modules['db'][func_name](*args,**kwargs)
if isinstance(f, (tornado.concurrent.Future, concurrent.futures.Future)):
f = yield tornado.gen.with_timeout(timedelta(seconds=120),f)
except Exception:
logger.warning('db_call error for %s',func_name,exc_info=True)
raise
raise tornado.gen.Return(f)
[docs] def get(self):
"""GET is invalid and returns an error"""
raise tornado.web.HTTPError(400,'GET is invalid. Use POST')
[docs] def post(self):
"""POST is invalid and returns an error"""
raise tornado.web.HTTPError(400,'POST is invalid. Use GET')
[docs] def set_default_headers(self):
self._headers['Server'] = 'IceProd/' + iceprod.__version__
[docs]class JSONRPCHandler(MyHandler):
"""JSONRPC 2.0 Handler.
Call DB methods using RPC over json.
"""
[docs] def check_xsrf_cookie(self):
pass
@tornado.gen.coroutine
[docs] def post(self):
"""Parses json in the jsonrpc format, returning results in
jsonrpc format as well.
"""
# parse JSON
try:
request = tornado.escape.json_decode(self.request.body)
except Exception as e:
self.json_error({'code':-32700,'message':'Parse Error',
'data':'invalid json'})
# check for all parts of jsonrpc 2.0 spec
if 'jsonrpc' not in request or (request['jsonrpc'] != '2.0' and
request['jsonrpc'] != 2.0):
self.json_error({'code':-32600,'message':'Invalid Request',
'data':'jsonrpc is not 2.0'})
return
if 'method' not in request:
self.json_error({'code':-32600,'message':'Invalid Request',
'data':'method not in request'})
return
if request['method'].startswith('_'):
self.json_error({'code':-32600,'message':'Invalid Request',
'data':'method name cannot start with underscore'})
return
self.statsd.incr('jsonrpc.'+request['method'])
# add rpc_ to method name to prevent calling other DB methods
method = 'rpc_'+request['method']
if 'params' in request:
params = request['params']
else:
params = {}
if 'id' in request:
request_id = request['id']
else:
request_id = None
if method.startswith("rpc_public"):
if isinstance(params,dict):
params.pop('passkey',None)
else:
# check for auth
if (isinstance(params,dict) and 'passkey' not in params) or (not params):
self.json_error({'code':403,'message':'Not Authorized',
'data':'missing passkey'},
request_id=request_id)
return
passkey = params.pop('passkey') if isinstance(params,dict) else params.pop(0)
try:
if ((isinstance(params,dict) and 'site_id' in params) or
isinstance(params,(tuple,list))):
# authorize site
site_id = params.pop('site_id') if isinstance(params,dict) else params.pop(0)
yield self.db_call('auth_authorize_site',site_id=site_id,key=passkey)
else:
# authorize task
yield self.db_call('auth_authorize_task',key=passkey)
except Exception:
logger.info('auth error', exc_info=True)
self.json_error({'code':403,'message':'Not Authorized',
'data':'passkey invalid'},
request_id=request_id)
return
# check for args and kwargs
if isinstance(params,dict):
args = params.pop('args') if 'args' in params else []
else:
args = params
params = {}
# call method on DB if exists
try:
ret = yield self.db_call(method,*args,**params)
except KeyError:
logger.info('DB method not found: %r', method)
self.json_error({'code':-32601,'message':'Method not found'},
request_id=request_id)
except tornado.gen.TimeoutError:
logger.info('Timeout error in DB method: %r', method, exc_info=True)
self.json_error({'code':-32001,'message':'Server error'},
status=503, request_id=request_id)
except Exception:
logger.info('error in DB method: %r', method, exc_info=True)
self.json_error({'code':-32000,'message':'Server error'},
status=500, request_id=request_id)
else:
if request_id is not None:
try:
self.write({'jsonrpc':'2.0', 'result':ret, 'id':request_id})
except:
logger.info('jsonrpc response: %r', ret)
raise
[docs] def json_error(self,error,status=400,request_id=None):
"""Create a proper jsonrpc error message"""
self.statsd.incr('jsonrpc_error')
self.set_status(status)
if isinstance(error,Exception):
error = str(error)
logger.info('json_error: %r',error)
if request_id is not None:
self.write({'jsonrpc':'2.0', 'error':error, 'id':request_id})
[docs]class LibHandler(MyHandler):
"""Handler for iceprod library downloads.
These are straight http downloads like normal.
"""
[docs] def initialize(self, prefix, directory, **kwargs):
"""
Get some params from the website module
:param fileio: AsyncFileIO object
:param prefix: library url prefix
:param directory: library directory on disk
"""
super(LibHandler,self).initialize(**kwargs)
self.prefix = prefix
self.directory = directory
@tornado.gen.coroutine
[docs] def get(self):
"""Look up a library"""
self.statsd.incr('lib')
try:
url = self.request.uri[len(self.prefix):]
except Exception:
url = ''
if not url:
# TODO: make this human-browsable in future
raise tornado.web.HTTPError(404,'not browsable')
else:
# TODO: make this work better for multi-site
filename = os.path.join(self.directory,url)
# open the file and send it
file = yield self.fileio.open(filename)
num = 65536
data = yield self.fileio.read(file,bytes=num)
self.write(data)
self.flush()
while len(data) >= num:
data = yield self.fileio.read(file,bytes=num)
self.write(data)
self.flush()
yield self.fileio.close(file)
[docs]class PublicHandler(MyHandler):
"""Handler for public facing website"""
[docs] def get_template_namespace(self):
namespace = super(MyHandler,self).get_template_namespace()
namespace['version'] = iceprod.__version__
namespace['section'] = self.request.uri.lstrip('/').split('?')[0].split('/')[0]
namespace['master'] = ('master' in self.cfg and
'status' in self.cfg['master'] and
self.cfg['master']['status'])
namespace['master_url'] = ('master' in self.cfg and
'url' in self.cfg['master'] and
self.cfg['master']['url'])
namespace['site_id'] = (self.cfg['site_id'] if 'site_id' in self.cfg else None)
namespace['sites'] = (self.cfg['webserver']['sites'] if (
'webserver' in self.cfg and
'sites' in self.cfg['webserver']) else None)
return namespace
[docs] def get_current_user(self):
user = self.get_secure_cookie("user", max_age_days=1)
user_secure = self.get_secure_cookie("user_secure", max_age_days=0.01)
self.current_user_secure = (user_secure is not None)
if user_secure is None or user == user_secure:
return user
else:
return None
[docs] def write_error(self,status_code=500,**kwargs):
"""Write out custom error page."""
self.set_status(status_code)
if status_code >= 500:
self.write('<h2>Internal Error</h2>')
else:
self.write('<h2>Request Error</h2>')
if 'message' in kwargs:
self.write('<br />'.join(kwargs['message'].split('\n')))
self.finish()
[docs]class Default(PublicHandler):
"""Handle / urls"""
@catch_error
@tornado.gen.coroutine
[docs] def get(self):
self.statsd.incr('default')
datasets = yield self.db_call('web_get_datasets',groups=['status'])
if isinstance(datasets,Exception):
raise datasets
if not datasets:
logger.info('no datasets to display: %r',datasets)
datasets = [] # set to iterable to prevent None error
self.render('main.html',datasets=datasets)
[docs]class Submit(PublicHandler):
"""Handle /submit urls"""
@catch_error
@tornado.web.authenticated
@tornado.gen.coroutine
[docs] def get(self):
self.statsd.incr('submit')
url = self.request.uri[1:]
passkey = yield self.db_call('auth_new_passkey')
if isinstance(passkey,Exception):
raise passkey
grids = yield self.db_call('web_get_gridspec')
if isinstance(grids,Exception):
raise grids
render_args = {
'passkey':passkey,
'grids':grids,
'edit':False,
'dataset':None,
'config':None,
}
self.render('submit.html',**render_args)
[docs]class Config(PublicHandler):
"""Handle /submit urls"""
@catch_error
@tornado.web.authenticated
@tornado.gen.coroutine
[docs] def get(self):
self.statsd.incr('config')
dataset_id = self.get_argument('dataset_id',default=None)
if not dataset_id:
self.write_error(400,message='must provide dataset_id')
return
dataset = yield self.db_call('web_get_datasets_details',dataset_id=dataset_id)
if isinstance(dataset,Exception):
raise dataset
if dataset_id not in dataset:
raise Exception('get_dataset_details does not have dataset_id '+dataset_id)
dataset = dataset[dataset_id]
edit = self.get_argument('edit',default=False)
if edit:
passkey = yield self.db_call('auth_new_passkey')
if isinstance(passkey,Exception):
raise passkey
else:
passkey = None
config = yield self.db_call('queue_get_cfg_for_dataset',dataset_id=dataset_id)
if isinstance(config,Exception):
raise config
render_args = {
'edit':edit,
'passkey':passkey,
'grids':None,
'dataset':dataset,
'config':config,
}
self.render('submit.html',**render_args)
[docs]class Site(PublicHandler):
"""Handle /site urls"""
@catch_error
@tornado.web.authenticated
@tornado.gen.coroutine
[docs] def get(self,url):
self.statsd.incr('site')
if url:
url_parts = [x for x in url.split('/') if x]
def cb(m):
print(m)
running_modules = self.modules['daemon']['get_running_modules']()
module_state = []
for mod in self.cfg['modules']:
state = mod in running_modules
module_state.append([mod, state])
passkey = yield self.db_call('auth_new_passkey')
if isinstance(passkey,Exception):
raise passkey
config = self.config.save_to_string()
self.render('site.html', url = url[1:], modules = module_state, passkey=passkey, config = config)
'''
filter_options = {}
filter_results = {n:self.get_arguments(n) for n in filter_options}
if url and url_parts:
site_id = url_parts[0]
ret = yield self.db_call('web_get_site_details',dataset_id=dataset_id)
if isinstance(ret,Exception):
raise ret
if ret:
site = ret.values()[0]
else:
site = None
tasks = yield self.db_call('web_get_tasks_by_status',site_id=site_id)
if isinstance(tasks,Exception):
raise tasks
self.render('site_detail.html',site_id=site_id,
site=site,tasks=tasks)
else:
sites = yield self.db_call('web_get_sites',**filter_results)
if isinstance(sites,Exception):
raise sites
self.render('site_browse.html',sites=sites,
filter_options=filter_options,
filter_results=filter_results)
'''
[docs]class Dataset(PublicHandler):
"""Handle /dataset urls"""
@catch_error
@tornado.gen.coroutine
[docs] def get(self,url):
self.statsd.incr('dataset')
if url:
url_parts = [x for x in url.split('/') if x]
filter_options = {'status':DBAPI.status_options['dataset']}
filter_results = {n:self.get_arguments(n) for n in filter_options}
if url and url_parts:
dataset_id = url_parts[0]
ret = None
if dataset_id.isdigit():
try:
if int(dataset_id) < 10000000:
try_dataset_id = GlobalID.globalID_gen(int(dataset_id),self.cfg['site_id'])
ret = yield self.db_call('web_get_datasets_details',
dataset_id=try_dataset_id)
if isinstance(ret,Exception):
ret = None
elif ret:
dataset_num = dataset_id
dataset_id = try_dataset_id
except Exception:
pass
if not ret:
ret = yield self.db_call('web_get_datasets_details',dataset_id=dataset_id)
if isinstance(ret,Exception):
raise ret
dataset_num = GlobalID.localID_ret(dataset_id,type='int')
if ret:
dataset = list(ret.values())[0]
else:
raise Exception('dataset not found')
passkey = yield self.db_call('auth_new_passkey')
if isinstance(passkey,Exception):
raise passkey
jobs = yield self.db_call('web_get_job_counts_by_status',
dataset_id=dataset_id)
tasks = yield self.db_call('web_get_tasks_by_status',
dataset_id=dataset_id)
task_info = yield self.db_call('web_get_task_completion_stats', dataset_id=dataset_id)
self.render('dataset_detail.html',dataset_id=dataset_id,dataset_num=dataset_num,
dataset=dataset,jobs=jobs,tasks=tasks,task_info=task_info,passkey=passkey)
else:
datasets = yield self.db_call('web_get_datasets',**filter_results)
if isinstance(datasets,Exception):
raise datasets
self.render('dataset_browse.html',datasets=datasets,
filter_options=filter_options,
filter_results=filter_results)
[docs]class Task(PublicHandler):
"""Handle /task urls"""
@catch_error
@tornado.gen.coroutine
[docs] def get(self,url):
self.statsd.incr('task')
if url:
url_parts = [x for x in url.split('/') if x]
dataset_id = self.get_argument('dataset_id',default=None)
status = self.get_argument('status',default=None)
passkey = yield self.db_call('auth_new_passkey')
if isinstance(passkey,Exception):
raise passkey
if url and url_parts:
if dataset_id and dataset_id.isdigit():
try:
if int(dataset_id) < 10000000:
try_dataset_id = GlobalID.globalID_gen(int(dataset_id),self.cfg['site_id'])
ret = yield self.db_call('web_get_datasets_details',
dataset_id=try_dataset_id)
if isinstance(ret,Exception):
ret = None
elif ret:
dataset_num = dataset_id
dataset_id = try_dataset_id
except Exception:
pass
task_id = url_parts[0]
ret = yield self.db_call('web_get_tasks_details',task_id=task_id,
dataset_id=dataset_id)
if ret:
task_details = list(ret.values())[0]
else:
task_details = None
logs = yield self.db_call('web_get_logs',task_id=task_id,lines=40) #TODO: make lines adjustable
del task_details['task_status'] # task_status and status are repeats. Remove task_status.
self.render('task_detail.html',task=task_details,logs=logs,passkey=passkey)
elif status:
tasks = yield self.db_call('web_get_tasks_details',status=status,
dataset_id=dataset_id)
if isinstance(tasks,Exception):
raise tasks
self.render('task_browse.html',tasks=tasks, passkey=passkey)
else:
status = yield self.db_call('web_get_tasks_by_status',dataset_id=dataset_id)
if isinstance(status,Exception):
raise status
self.render('tasks.html',status=status)
[docs]class Job(PublicHandler):
"""Handle /job urls"""
@catch_error
@tornado.gen.coroutine
[docs] def get(self,url):
self.statsd.incr('job')
if url:
url_parts = [x for x in url.split('/') if x]
dataset_id = self.get_argument('dataset_id',default=None)
status = self.get_argument('status',default=None)
passkey = yield self.db_call('auth_new_passkey')
if isinstance(passkey,Exception):
raise passkey
if url and url_parts:
job_id = url_parts[0]
ret = yield self.db_call('web_get_jobs_details',job_id=job_id)
if isinstance(ret,Exception):
raise ret
if ret:
job_details = ret
else:
job_details = {}
self.render('job_detail.html', job=job_details, passkey=passkey)
else:
if dataset_id and dataset_id.isdigit():
try:
if int(dataset_id) < 10000000:
try_dataset_id = GlobalID.globalID_gen(int(dataset_id),self.cfg['site_id'])
ret = yield self.db_call('web_get_datasets_details',
dataset_id=try_dataset_id)
if isinstance(ret,Exception):
ret = None
elif ret:
dataset_num = dataset_id
dataset_id = try_dataset_id
except Exception:
pass
jobs = yield self.db_call('web_get_jobs_by_status', status=status,
dataset_id=dataset_id)
if isinstance(jobs,Exception):
raise jobs
self.render('job_browse.html', jobs=jobs, passkey=passkey)
[docs]class Documentation(PublicHandler):
@catch_error
[docs] def get(self, url):
self.statsd.incr('documentation')
doc_path = get_pkgdata_filename('iceprod.server','data/docs')
self.write(documentation.load_doc(doc_path+'/' + url))
self.flush()
[docs]class Log(PublicHandler):
@catch_error
@tornado.gen.coroutine
[docs] def get(self, url, log):
self.statsd.incr('log')
logs = yield self.db_call('web_get_logs',task_id=url)
log_text = logs[log]
html = '<html><body>'
html += log_text.replace('\n', '<br/>')
html += '</body></html>'
self.write(html)
self.flush()
[docs]class GroupsHandler(PublicHandler):
"""View/modify groups"""
@catch_error
@tornado.gen.coroutine
[docs] def get(self):
render_args = {
'edit': True if self.current_user else False,
}
render_args['groups'] = yield self.db_call('rpc_get_groups')
if render_args['edit']:
passkey = yield self.db_call('auth_new_passkey')
if isinstance(passkey,Exception):
raise passkey
render_args['passkey'] = passkey
self.render('groups.html', **render_args)
[docs]class UserAccount(PublicHandler):
"""View/modify a user account"""
@catch_error
@authenticated_secure
@tornado.gen.coroutine
[docs] def get(self):
username = self.get_argument('username', default=self.current_user)
account = yield self.db_call('website_get_user_account')
if isinstance(account, Exception):
raise account
self.render('user_account.html', account=account)
@catch_error
@authenticated_secure
@tornado.gen.coroutine
[docs] def post(self):
username = self.get_argument('username', default=self.current_user)
password = self.get_argument('password', default=None)
if not password:
raise Exception('invalid password')
ret = yield self.db_call('website_edit_user_account', password=password)
if isinstance(ret, Exception):
raise ret
self.get()
[docs]class Help(PublicHandler):
"""Help Page"""
@catch_error
[docs] def get(self):
self.statsd.incr('help')
self.render('help.html')
[docs]class Other(PublicHandler):
"""Handle any other urls - this is basically all 404"""
@catch_error
[docs] def get(self):
self.statsd.incr('other')
path = self.request.path
self.set_status(404)
self.render('404.html',path=path)
[docs]class Login(PublicHandler):
"""Handle the login url"""
@catch_error
[docs] def get(self):
self.statsd.incr('login')
n = self.get_argument('next', default='/')
secure = self.get_argument('secure', default=None)
if 'password' in self.cfg['webserver']:
self.render('login.html', status=None, next=n)
else:
# TODO: remove this entirely
if secure:
self.set_secure_cookie('user_secure', 'admin', expires_days=0.01)
self.set_secure_cookie('user', 'admin', expires_days=1)
self.redirect(n)
@catch_error
[docs] def post(self):
n = self.get_argument('next', default='/')
secure = self.get_argument('secure', default=None)
if ('password' in self.cfg['webserver'] and
self.get_argument('pwd') == self.cfg['webserver']['password']):
if secure:
self.set_secure_cookie('user_secure', 'admin', expires_days=0.01)
self.set_secure_cookie('user', 'admin', expires_days=1)
self.redirect(n)
else:
self.render('login.html', status='failed', next=n)
[docs]class Logout(PublicHandler):
@catch_error
[docs] def get(self):
self.statsd.incr('logout')
self.clear_cookie("user")
self.clear_cookie("user_secure")
self.current_user = None
self.render('logout.html', status=None)