Commit bb86ce58 authored by Daniele Venzano's avatar Daniele Venzano 🏇

Merge branch 'devel/zapp-shop' into 'master'

New web UI and lots of fixes

See merge request !15
parents c331579e 7c1b6dae
......@@ -7,7 +7,7 @@ The main Zoe Components are:
* zoe master: the core component that performs application scheduling and talks to Swarm
* zoe api: the Zoe frontend, offering a web interface and a REST API
* zoe: command-line client
* command-line clients (zoe.py and zoe-admin.py)
The Zoe master is the core component of Zoe and communicates with the clients by using an internal ZeroMQ-based protocol. This protocol is designed to be robust, using the best practices from ZeroMQ documentation. A crash of the Api or of the Master process will not leave the other component inoperable, and when the faulted process restarts, work will restart where it was left.
......
......@@ -62,7 +62,7 @@ Master options:
Authentication:
* ``auth-type = text`` : Authentication type (text or ldap)
* ``auth-type = text`` : Authentication type (text, ldap or ldapsasl)
* ``auth-file = zoepass.csv`` : Path to the CSV file containing user,pass,role lines for text authentication
* ``ldap-server-uri = ldap://localhost`` : LDAP server to use for user authentication
* ``ldap-base-dn = ou=something,dc=any,dc=local`` : LDAP base DN for users
......@@ -77,6 +77,10 @@ Scheduler options:
Default options for the scheduler enable the traditional Zoe scheduler that was already available in the previous releases.
ZApp shop:
* ``zapp-shop-path = /var/lib/zoe-apps`` : Path where ZApp folders are stored
Backend choice:
* ``backend = <Swarm|Kubernetes>`` : cluster back-end to use to run ZApps
......
......@@ -9,6 +9,12 @@ To better work together we have established some rules on how to contribute.
If you need ideas on features that are waiting to be implemented, you can check the `roadmap <https://github.com/DistributedSystemsGroup/zoe/wiki/RoadMap>`_.
Development repository
----------------------
Development happens at `Eurecom's GitLab repository <https://gitlab.eurecom.fr/zoe/main>`_. The GitHub repository is a read-only mirror.
The choice of GitLab over GitHub is due to the CI pipeline that we set-up to test Zoe.
Bug reports and feature requests
--------------------------------
......
......@@ -3,26 +3,41 @@
Installing Zoe
==============
Multiple deployment options are available:
If you are looking for the five-minutes install procedure, just for testing, check the :ref:`test install <test-install-label>` section, below.
* Demo install via Docker Compose
* Deployment scripts
* Manual install
When installing Zoe for production you should, first of all, look at the following requirements and take a decision about each of them:
The section below describes how to install Zoe manually.
* Container back-end
* Shared filesystem: we know of NFS or CephFS, but anything similar should work
* Network: how your users will connect to the containers
* Authentication back-end: how your users will authenticate to Zoe (LDAP or text file)
* How to manage Zoe Applications (ZApps)
* ZApp output logs: see :ref:`logging`
Overview
--------
After, you can start the installation, as outlined in the :ref:`manual install <manual-install-label>` section.
Zoe components:
Choosing the container back-end
-------------------------------
* Master
* API
* command-line client
At this time Zoe supports three back-ends:
* Docker engine: useful for running Zoe on a single PC, it is the simplest to setup.
* Legacy Docker Swarm: simple to install, additional features like SSL, high-availability and dynamic host discovery can be added as needed. Please note that Zoe does not support the new Swarm Mode of Docker Engine as the API is too limited.
* Kubernetes: the most complex to setup, we suggest using it only if you already have (or need) a Kubernetes setup for running other software.
Shared filesystem
-----------------
Users need to put data and binaries in a place accessible by Zoe and need to be able to access the results and the logs generated by running ZApp.
Zoe uses the concept of workspaces: each user has a private directory that is attached to all the containers of each ZApp belonging to her in a well-known location. This filesystem can be accessed by a special gateway container spawned by the administrator (see `gateway containers <https://github.com/DistributedSystemsGroup/gateway-containers>`_) or by other methods (direct mount on user machines, webdav, web file managers).
Zoe implements a "directory" back-end for workspaces. Container back-ends may implement more volume technologies: Zoe is not involved, it needs only the information on how to attach the user volume to the container, so the effort required to support new volume types should be minimal.
Optionally an API manager can be configured in front of the Zoe API to provide SSL termination and usage statistics:
At Eurecom we use CephFS, but we know of successful Zoe deployments based on NFS.
* :ref:`api-manager-label`
Networking
----------
Most of the ZApps expose a number of interfaces (web, REST and others) to the user. Zoe configures the active back-end to expose these ports, but does not perform any additional action to configure routing or DNS to make the ports accessible. Keeping in mind that the back-end network configuration is outside Zoe's competence area, here there a non-exhaustive list of the possible configurations:
......@@ -30,18 +45,136 @@ Most of the ZApps expose a number of interfaces (web, REST and others) to the us
* use a proxy, like the one developed for Zoe: :ref:`proxy`
* use back-end network plugins to build custom topologies
Stand-alone environment for development and testing
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Authentication back-end
-----------------------
Zoe has a simple user model: users are authenticated against an external source of truth, that assigns also one of three roles:
* guest: cannot access the API (and the command-line tools) and can run one execution at a time
* user: can use the API and has no limits on executions
* admin: can operate on executions belonging to other users, can delete records of past executions
Zoe supports two authentication back-ends:
* LDAP and LDAP+SASL
* Text file
A simple deployment for development and testing is possible with just:
As most of Zoe, the authentication back-end is pluggable and others can be easily implemented.
LDAP
^^^^
Plain LDAP or LDAP+SASL GSSAPI are available.
The implementation will use the ``uid`` as the user_id and ``gidNumber`` to decide the role.
In Zoe configuration you need to specify the mapping between group IDs and roles.
Text file
^^^^^^^^^
For testing and for simple deployments with a few users, a CSV text file can be used.
* A Docker Engine
* Zoe
Its format is::
Please note: since Zoe will use the Swarm API to talk to the Docker Engine, the addresses of the exposed ports for running ZApp may be ``0.0.0.0``, causing the links generated by the web interface and the command line tool to be wrong. ZApps can be accessed by using ``127.0.0.1`` instead.
<username>,<password>,<role>
The file location can be specified in the ``zoe.conf`` file and it needs to be readable only be the Zoe processes.
Managing Zoe applications
-------------------------
At the very base, ZApps are composed of a container image and a JSON description. The container image can be stored in a local, private, registry, or in a public one, accessible via the Internet.
Zoe does not provide a way to automatically build images, push them to a local registry, or pull them to the hosts when needed. At Eurecom we provide an automated environment based on GItLab's CI features: users are able to customize their applications (JSON and Dockerfiles) by working on git repositories. Images are rebuilt and pushed on commit and JSON files are generated and copied to the ZApp shop directory. You can check out how we do it here:
https://gitlab.eurecom.fr/zoe-apps
The ZApp Shop
^^^^^^^^^^^^^
The Zoe web interface provides a ZApp shop to showcase available ZApps and have a friendly and easy way for users to list and access ZApps.
The shop is managed locally. It looks for ZApps in a configured directory (option ``zapp-shop-path``). Each ZApp must live in its own directory, that must contain:
* manifest.json : a JSON file that describes the contents of the ZApp
* a logo that is displayed on the web interface
* one or more text files in markdown format with ZApp information and documentation
* one or more JSON Zoe application descriptions
The ``manifest.json`` file drives the ZApp Shop. Its format is as follows::
{
"version": 1,
"zapps": [
{
"category": "TensorFlow",
"name": "Google TensorFlow notebook",
"description": "tf-google.json",
"readable_descr": "README-goog.md",
"parameters": []
},
{
"category": "TensorFlow",
"name": "Google TensorFlow batch",
"description": "tf-google.json",
"readable_descr": "README-batch.md",
"parameters": [
{
"kind": "command",
"name": "tf-jupyter",
"readable_name": "Command",
"description": "The Python script to run, relative to the workspace directory",
"type": "string",
"default": "./my-tf-app/main.py"
}
]
},
{
"category": "TensorFlow",
"name": "DRAGNN SyntaxNet model",
"description": "stnet-google.json",
"readable_descr": "README-syntaxnet.md",
"parameters": []
},
{
"category": "TensorFlow",
"name": "Magenta model",
"description": "mag-google.json",
"readable_descr": "README-magenta.md",
"parameters": []
}
]
}
* version : a internal version, used by Zoe to recognize the manifest format. For now only 1 is supported.
* zapps : a list of related ZApps that have to be shown in the shop
For each ZApp:
* category : the category this ZApp belongs to, it is used to group ZApps in the web interfaces
* name : the human-readable name
* description : the name of the json file with the Zoe description
* readable_descr : the name of the markdown file containing user documentation for the ZApp
* parameters : a list of parameters the user can set to tune the ZApp before starting it
Parameters:
Parameters are values of the JSON description that are modified at run time.
* kind : the kind of parameter, it can be ``command`` or ``environment``
* name : the machine-friendly name of the parameter
* readable_name : the human-friendly name of the parameter
* description : an helpful description
* type : string or integer, used for basic for validation
* default : the default value
Parameters can be of two kinds:
* environment : the parameter is passed as an environment variable. The name of the environment variable is stored in the ``name`` field. The JSON description is modified by setting the user-defined value in the environment variable with the corresponding name. All services that have the variable defined are modified.
* command : the service named ``name`` has its start-up command changed to the user-defined value
To get you started, here there is an :download:`archive <figures/zapp-shop-demo.tar.gz>` containing some ZApps. Decompress it in your ZApp shop directory to have some Zapps to play with.
Example of distributed environment
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
----------------------------------
For running heavier workloads and distributed applications, you need a real container cluster. In this example we will use Docker Swarm, as it is simpler to setup than Kubernetes.
......@@ -71,36 +204,35 @@ To configure Swarm and the Registry, please refer to the documentation available
To configure container networking, we suggest the standard Swarm overlay network.
Users need to put data and binaries in a place accessible by Zoe and need to be able to access the results and the logs generated by running ZApp.
Zoe uses the concept of workspaces: each user has a private directory that is attached to all the containers of each ZApp belonging to her in a well-known location. This filesystem can be accessed by a special gateway container spawned by the administrator (see `gateway containers <https://github.com/DistributedSystemsGroup/gateway-containers>`_) or by other methods (direct mount on user machines, webdav, web file managers).
In this configuration Zoe expects the network filesystem to be mounted in the same location on all hosts registered in Swarm. This location is specified in the ``workspace-base-path`` Zoe configuration item. Zoe will create a directory under it named as ``deployment-name`` by default or ``workspace-deployment-path`` if specified. Under it a new directory will be created for each user accessing Zoe.
Zoe expects the network filesystem to be mounted in the same location on all hosts registered in Swarm. This location is specified in the ``workspace-base-path`` Zoe configuration item. Zoe will create a directory under it named as ``deployment-name`` by default or ``workspace-deployment-path`` if specified. Under it a new directory will be created for each user accessing Zoe.
.. _test-install-label:
Refer to the manual install section below for more details and links to external resources.
Stand-alone environment for development and testing
---------------------------------------------------
Choosing the back-end
---------------------
A simple deployment for development and testing is possible with just:
At this time Zoe supports three back-ends:
* A Docker Engine
* Zoe
* Docker engine: useful for running Zoe on a single PC, it is the simplest to setup.
* Docker Swarm: simple to install, additional features like SSL, high-availability and dynamic host discovery can be added as needed. Please note that Zoe does not support the new Swarm Mode of Docker Engine as the API is too limited.
* Kubernetes: the most complex to setup, we suggest using it only if you already have (or need) a Kubernetes setup for running other software.
Please note: since Zoe will use the Swarm API to talk to the Docker Engine, the addresses of the exposed ports for running ZApp may be ``0.0.0.0``, causing the links generated by the web interface and the command line tool to be wrong. ZApps can be accessed by using ``127.0.0.1`` instead.
Docker compose
--------------
^^^^^^^^^^^^^^
In the root of the repository you can find a ``docker-compose.yml`` file that should help get you started.
Look also at the deployment scripts below, as they provide an option for a simple, zoe-on-a-laptop, install.
Deployment scripts
------------------
^^^^^^^^^^^^^^^^^^
Refer to `zoe-deploy <https://github.com/DistributedSystemsGroup/zoe-deploy>`_ repository for automated deployment scripts for configurations with Swarm or Kubernetes back-ends.
Manual install
--------------
.. _manual-install-label:
Manual install (recommended for production)
-------------------------------------------
This section shows how to install the components outlined in the distributed environment outlined above. A lot of other options and possibilities exist for deploying Zoe.
......@@ -139,7 +271,7 @@ Images: Docker Hub Vs local Docker registry
A few sample ZApps have their images available on the Docker Hub. We strongly suggest setting up a private registry, containing your customized Zoe Service images.
Zoe
---
^^^
Zoe is written in Python and uses the ``requirements.txt`` file to list the package dependencies needed for all components of Zoe. Not all of them are needed in all cases, for example you need the ``kazoo`` library only if you use Zookeeper to manage Swarm high availability.
......
......@@ -17,7 +17,6 @@
from datetime import datetime, timedelta
import logging
import re
import os
import zoe_api.exceptions
......@@ -29,6 +28,8 @@ from zoe_lib.config import get_conf
log = logging.getLogger(__name__)
GUEST_QUOTA_MAX_EXECUTIONS = 1
class APIEndpoint:
"""
......@@ -71,10 +72,14 @@ class APIEndpoint:
except zoe_lib.exceptions.InvalidApplicationDescription as e:
raise zoe_api.exceptions.ZoeException('Invalid application description: ' + e.message)
if 3 > len(exec_name) > 128:
raise zoe_api.exceptions.ZoeException("Execution name must be between 4 and 128 characters long")
if not re.match(r'^[a-zA-Z0-9\-]+$', exec_name):
raise zoe_api.exceptions.ZoeException("Execution name can contain only letters, numbers and dashes. '{}' is not valid.".format(exec_name))
# quota check
if role == "guest":
running_execs = self.execution_list(uid, role, **{'status': 'running'})
running_execs += self.execution_list(uid, role, **{'status': 'starting'})
running_execs += self.execution_list(uid, role, **{'status': 'scheduled'})
running_execs += self.execution_list(uid, role, **{'status': 'submitted'})
if len(running_execs) > GUEST_QUOTA_MAX_EXECUTIONS:
raise zoe_api.exceptions.ZoeException('Guest users cannot run more than one execution at a time, quota exceeded.')
new_id = self.sql.execution_new(exec_name, uid, application_description)
success, message = self.master.execution_start(new_id)
......@@ -100,6 +105,9 @@ class APIEndpoint:
def execution_delete(self, uid, role, exec_id):
"""Delete an execution."""
if role != "admin":
raise zoe_api.exceptions.ZoeAuthException()
e = self.sql.execution_list(id=exec_id, only_one=True)
assert isinstance(e, zoe_lib.state.sql_manager.Execution)
if e is None:
......@@ -194,7 +202,7 @@ class APIEndpoint:
for port in service.description['ports']:
port_key = str(port['port_number']) + "/" + port['protocol']
backend_port = self.sql.port_list(only_one=True, service_id=service.id, internal_name=port_key)
if backend_port.external_ip is not None:
if backend_port is not None and backend_port.external_ip is not None:
endpoint = port['url_template'].format(**{"ip_port": backend_port.external_ip + ":" + str(backend_port.external_port)})
endpoints.append((port['name'], endpoint))
......
......@@ -48,6 +48,8 @@ def zoe_web_main() -> int:
if args.log_file != "stderr":
log_args['filename'] = args.log_file
logging.basicConfig(**log_args)
logging.getLogger("MARKDOWN").setLevel(logging.WARNING)
logging.getLogger("tornado").setLevel(logging.WARNING)
if config.get_conf().auth_type == 'ldap' and not zoe_api.auth.ldap.LDAP_AVAILABLE:
log.error("LDAP authentication requested, but 'pyldap' module not installed.")
......
......@@ -73,7 +73,9 @@ def get_auth(handler: tornado.web.RequestHandler):
if handler.get_secure_cookie('zoe'):
cookie_val = str(handler.get_secure_cookie('zoe'))
uid, role = cookie_val[2:-1].split('.')
log.info('Authentication done using cookie')
log.debug('Authentication done using cookie')
if role == "guest":
raise ZoeRestAPIException('Guest users cannot use the API, ask for a role upgrade', 401, {'WWW-Authenticate': 'Basic realm="Login Required"'})
return uid, role
auth_header = handler.request.headers.get('Authorization')
......@@ -118,6 +120,9 @@ def get_auth(handler: tornado.web.RequestHandler):
raise ZoeRestAPIException('missing or wrong authentication information', 401, {'WWW-Authenticate': 'Basic realm="Login Required"'})
log.debug('Authentication done using auth-mechanism')
if role == "guest":
raise ZoeRestAPIException('Guest users cannot use the API, ask for a role upgrade', 401, {'WWW-Authenticate': 'Basic realm="Login Required"'})
return uid, role
......
sonar.projectName=zoe_api
sonar.host.url=http://your-sonarqube-server-address
sonar.sources=.
sonar.language=py
sonar.sourceEncoding=UTF-8
......@@ -22,6 +22,7 @@ import tornado.web
import zoe_api.web.start
import zoe_api.web.websockets
import zoe_api.web.executions
import zoe_api.web.zapp_shop
from zoe_lib.version import ZOE_API_VERSION, ZOE_VERSION
......@@ -38,15 +39,18 @@ def web_init(api_endpoint) -> List[tornado.web.URLSpec]:
tornado.web.url(r'/login', zoe_api.web.start.LoginWeb, route_args, name='login'),
tornado.web.url(r'/logout', zoe_api.web.start.LogoutWeb, route_args, name='logout'),
tornado.web.url(r'/executions/new', zoe_api.web.executions.ExecutionDefineWeb, route_args, name='execution_define'),
tornado.web.url(r'/executions', zoe_api.web.executions.ExecutionListWeb, route_args, name='execution_list'),
tornado.web.url(r'/executions/start', zoe_api.web.executions.ExecutionStartWeb, route_args, name='execution_start'),
tornado.web.url(r'/executions/restart/([0-9]+)', zoe_api.web.executions.ExecutionRestartWeb, route_args, name='execution_restart'),
tornado.web.url(r'/executions/terminate/([0-9]+)', zoe_api.web.executions.ExecutionTerminateWeb, route_args, name='execution_terminate'),
tornado.web.url(r'/executions/delete/([0-9]+)', zoe_api.web.executions.ExecutionDeleteWeb, route_args, name='execution_delete'),
tornado.web.url(r'/executions/inspect/([0-9]+)', zoe_api.web.executions.ExecutionInspectWeb, route_args, name='execution_inspect'),
tornado.web.url(r'/service/logs/([0-9]+)', zoe_api.web.executions.ServiceLogsWeb, route_args, name='service_logs'),
tornado.web.url(r'/websocket', zoe_api.web.websockets.WebSocketEndpointWeb, route_args, name='websocket')
tornado.web.url(r'/websocket', zoe_api.web.websockets.WebSocketEndpointWeb, route_args, name='websocket'),
tornado.web.url(r'/zapp-shop', zoe_api.web.zapp_shop.ZAppShopHomeWeb, route_args, name='zappshop'),
tornado.web.url(r'/zapp-shop/logo/([a-z\-.]+)', zoe_api.web.zapp_shop.ZAppLogoWeb, route_args, name='zappshop_logo'),
tornado.web.url(r'/zapp-shop/start/([0-9a-z\-.]+)', zoe_api.web.zapp_shop.ZAppStartWeb, route_args, name='zappshop_start')
]
return web_routes
......
......@@ -23,27 +23,6 @@ from zoe_api.api_endpoint import APIEndpoint # pylint: disable=unused-import
from zoe_api.web.custom_request_handler import ZoeRequestHandler
class ExecutionDefineWeb(ZoeRequestHandler):
"""Handler class"""
def initialize(self, **kwargs):
"""Initializes the request handler."""
super().initialize(**kwargs)
self.api_endpoint = kwargs['api_endpoint'] # type: APIEndpoint
@catch_exceptions
def get(self):
"""Define a new execution."""
uid, role = get_auth(self)
if uid is None:
return self.redirect(self.get_argument('next', u'/login'))
template_vars = {
"uid": uid,
"role": role,
}
self.render('execution_new.html', **template_vars)
class ExecutionStartWeb(ZoeRequestHandler):
"""Handler class"""
def initialize(self, **kwargs):
......@@ -67,7 +46,7 @@ class ExecutionStartWeb(ZoeRequestHandler):
self.redirect(self.reverse_url('execution_inspect', new_id))
class ExecutionRestartWeb(ZoeRequestHandler):
class ExecutionListWeb(ZoeRequestHandler):
"""Handler class"""
def initialize(self, **kwargs):
"""Initializes the request handler."""
......@@ -75,19 +54,23 @@ class ExecutionRestartWeb(ZoeRequestHandler):
self.api_endpoint = kwargs['api_endpoint'] # type: APIEndpoint
@catch_exceptions
def get(self, execution_id: int):
"""Restart an already defined (and not running) execution."""
def get(self):
"""Home page with authentication."""
uid, role = get_auth(self)
if uid is None:
return self.redirect(self.get_argument('next', u'/login'))
e = self.api_endpoint.execution_by_id(uid, role, execution_id)
new_id = self.api_endpoint.execution_start(uid, role, e.name, e.description)
executions = self.api_endpoint.execution_list(uid, role)
self.redirect(self.reverse_url('execution_inspect', new_id))
template_vars = {
"uid": uid,
"role": role,
'executions': sorted(executions, key=lambda e: e.id, reverse=True)
}
self.render('execution_list.html', **template_vars)
class ExecutionTerminateWeb(ZoeRequestHandler):
class ExecutionRestartWeb(ZoeRequestHandler):
"""Handler class"""
def initialize(self, **kwargs):
"""Initializes the request handler."""
......@@ -96,19 +79,18 @@ class ExecutionTerminateWeb(ZoeRequestHandler):
@catch_exceptions
def get(self, execution_id: int):
"""Terminate an execution."""
"""Restart an already defined (and not running) execution."""
uid, role = get_auth(self)
if uid is None:
return self.redirect(self.get_argument('next', u'/login'))
success, message = self.api_endpoint.execution_terminate(uid, role, execution_id)
if not success:
raise zoe_api.exceptions.ZoeException(message)
e = self.api_endpoint.execution_by_id(uid, role, execution_id)
new_id = self.api_endpoint.execution_start(uid, role, e.name, e.description)
self.redirect(self.reverse_url('home_user'))
self.redirect(self.reverse_url('execution_inspect', new_id))
class ExecutionDeleteWeb(ZoeRequestHandler):
class ExecutionTerminateWeb(ZoeRequestHandler):
"""Handler class"""
def initialize(self, **kwargs):
"""Initializes the request handler."""
......@@ -117,12 +99,12 @@ class ExecutionDeleteWeb(ZoeRequestHandler):
@catch_exceptions
def get(self, execution_id: int):
"""Delete an execution."""
"""Terminate an execution."""
uid, role = get_auth(self)
if uid is None:
return self.redirect(self.get_argument('next', u'/login'))
success, message = self.api_endpoint.execution_delete(uid, role, execution_id)
success, message = self.api_endpoint.execution_terminate(uid, role, execution_id)
if not success:
raise zoe_api.exceptions.ZoeException(message)
......
......@@ -86,22 +86,46 @@ class HomeWeb(ZoeRequestHandler):
if uid is None:
return self.redirect(self.get_argument('next', u'/login'))
if role == 'guest':
return self._aml_homepage(uid)
filters = {
"user_id": uid,
"limit": 5
}
last_executions = self.api_endpoint.execution_list(uid, role, **filters)
filters = {
"user_id": uid,
"status": "running"
}
last_running_executions = self.api_endpoint.execution_list(uid, role, **filters)
filters = {
"user_id": uid,
"status": "submitted"
}
last_running_executions += self.api_endpoint.execution_list(uid, role, **filters)
filters = {
"user_id": uid,
"status": "scheduled"
}
last_running_executions += self.api_endpoint.execution_list(uid, role, **filters)
filters = {
"user_id": uid,
"status": "starting"
}
last_running_executions += self.api_endpoint.execution_list(uid, role, **filters)
executions = self.api_endpoint.execution_list(uid, role)
running_reservations = [e.total_reservations for e in last_running_executions]
total_memory = sum([r.memory.max for r in running_reservations])
total_cores = sum([r.cores.max for r in running_reservations])
template_vars = {
"uid": uid,
"role": role,
'executions': sorted(executions, key=lambda e: e.id),
'is_admin': role == 'admin',
"total_memory": total_memory,
"total_cores": total_cores,
'last_executions': sorted(last_executions, key=lambda e: e.id),
'running_executions': sorted(last_running_executions, key=lambda e: e.id)
}
self.render('home_user.html', **template_vars)
def _aml_homepage(self, uid):
"""Home page for students of the AML course."""
template_vars = {
'uid': uid
}
return self.render('home_guest.html', **template_vars)
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
id="svg4147"
version="1.1"
inkscape:version="0.91 r13725"
xml:space="preserve"
width="379.92499"
height="271.62421"
viewBox="0 0 379.92499 271.6242"
sodipodi:docname="logo.svg"><metadata
id="metadata4153"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
id="defs4151"><clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath4161"><path
d="m 0,1.2207e-4 720,0 0,539.99999793 -720,0 L 0,1.2207e-4 Z"
id="path4163"
inkscape:connector-curvature="0"
style="clip-rule:evenodd" /></clipPath><clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath4179"><path
d="m 0,6.104e-6 720,0 L 720,540.00001 l -720,0 L 0,6.104e-6 Z"
id="path4181"
inkscape:connector-curvature="0"
style="clip-rule:evenodd" /></clipPath><mask
maskUnits="userSpaceOnUse"
x="0"
y="0"
width="1"
height="1"
id="mask4185"><image
width="1"
height="1"
preserveAspectRatio="none"
xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAHMAAAA3CAAAAAAa4SpAAAAAAXNCSVQI5gpbmQAABAFJREFUWIW1WTGLE0EU/nYIhysrFzCgQdnG6Gk4hO1SKKw2adOmTp3K5vr8gIBt/kAsLQKKjbCNcFXAQjyLFcTDQ10RQW1ci+zOvDc7M7t7cK+ayew335t5b97Me/FgEm+/dxPAn3f4YRyvly7aQL1omRNJjoaiHZ03XGQ77HIR9xsAglmWV+Ro0JyxN+fY7WTPDRCLKuEOGTVj7G4M4JVL5UFqoczzPAnrGcXcAt4EFoRnQxQy82ooI4fKYyOik7gp8zzxXYyezS47WRnMKmop8zx1kAbb1mBvXU/pIo3qwUlHw2iU2+U0juN5RZHU4vizCoPhyK25QzD3Sadq5iDi3r8xMXorPvlmJAD43WjKvWpJQT06Mtb80z+io7Mq5R53haRHxkLmWdR7if1NRymgkw71UZ8tJh1pw/TQZ8ofpmT5xkNIj26mKdVllOuqwQWJ3wupKIHYzj1xkhUb6DFnmRjBZH/LmK+WubGHGqIsXQqjTG23iPL/dbF2BetaKSGUzcnMA7pIe5wi4B1FLPtTOyUQSNWUXzLKlSMeRxqH1GHrDuIlLjVTGg4REbm7KQCEElV3R860rW1BSSJAH8CEaeCUOM3zxEwZ12ETqpxctdnPuXSVl7WjVF6zBjyJczitQVpSoiM/9tAvm9mFUgLyruiL6+VvL1tRnpDOo9dNIC/KxoHYL5uvLpYSx2VjpN7Lny6WEl/LxjUhXf9vY8reMek0pVScfXG5MZWkPCEe3phSyQ3xrWw2PSrnpfwlW+J92bp7sZS4VDZ+irOy+fB8lCKMe9avqVwpG+9JfGiS8rErOkaRL2xqUi8AwIgEXDlL5XHVgLKIYrZnL5UVwclHx6IWNtAp5QWR1OVPZD9DYCg7zhQIphirLohlDRZjakMhO0duVFyhJJzudw2wJ7doCZCNdi90UqVkj3H3K0M9N4cAcShzMlLIzETJttsVVBRHcTyUsnMbxmO1lFgNkAd+qud6SohqhQEj42xUfJYFsY9ISlZJMEsJlb9nZRQgVjG6QsjyA65Xh4AtpNQTpNVpkryohCOPJ7T6VgREodRgU5+aZa1+pz9nWv7Z44WCaq2nT8G6Rh2WvJJcEB6zVzofyMX2eQqdmeL5iH6xnah590ZrE7pYk/9Z25Vnz0+BsztPHrBfP977beDE/KkGPgWAx/f1725/YN0wbyAbm2O6S0OlxDosNBQ6NJnbA3kTUkOgChyVMxumBanJpYGOsyy1tZUJC5m4wHm+spllbN/fae0FOXQYx1WKFRMz0FoLZeBqMWwnaexWWEyqZm1aMIY/N1XcototAoIxiwObFoVxIJxRcLYwFfPNOnhXD4Fbh/jy5u33f20oAQCdAP6B40+L/3nrWx5j2o+vAAAAAElFTkSuQmCC"
id="image4187" /></mask></defs><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="640"
inkscape:window-height="480"
id="namedview4149"
showgrid="false"
inkscape:zoom="0.78462664"
inkscape:cx="-10.9329"
inkscape:cy="178.9348"
inkscape:current-layer="g4155"
fit-margin-top="10"
fit-margin-left="10"
fit-margin-right="10"
fit-margin-bottom="10" /><g
id="g4155"
inkscape:groupmode="layer"
inkscape:label="logo"
transform="matrix(1.25,0,0,-1.25,-219.625,427.6)"><path
d="m 273.4,308.82 c 5.56,0 10.36,-1.74 14.4,-5.21 4.03,-3.47 6.05,-7.83 6.05,-13.09 0,-3.21 -2.99,-6.3 -8.97,-9.29 l -1.29,-0.65 c -3.75,-1.87 -22.3,-20.98 -55.65,-57.31 -3.44,-3.7 -9.37,-10.95 -17.79,-21.74 -7.51,-10.74 -11.26,-17.1 -11.26,-19.08 0,-0.4 1.99,-0.61 5.97,-0.61 4.46,0 12.38,0.94 23.76,2.8 9.75,1.72 17.73,2.58 23.94,2.58 5.48,0 11.37,-0.44 17.67,-1.32 10.16,-1.31 15.24,-3.46 15.24,-6.45 0,-3.76 -2.52,-5.63 -7.56,-5.63 -1.35,0 -3.89,0.25 -7.61,0.75 -3.73,0.5 -5.99,0.75 -6.79,0.75 -5.23,0 -13.01,-0.79 -23.32,-2.36 l -21.53,-3.2 c -1.47,-0.14 -5.24,-1.06 -11.28,-2.75 -5.6,-1.62 -9.52,-2.43 -11.76,-2.43 -3.84,0 -6.87,1.75 -9.09,5.27 -1.84,2.3 -2.76,5.28 -2.76,8.95 0,4.24 4.34,12.91 13.01,26 8.29,12.28 15.76,21.88 22.44,28.79 l 33.38,38.67 c 11.4,13.07 17.1,20.16 17.1,21.26 0,0.41 -1.85,0.61 -5.54,0.61 -18.25,0 -36.31,-2.5 -54.17,-7.48 -2.48,-0.77 -4.09,-1.42 -4.83,-1.95 -0.74,-0.53 -1.04,-1.75 -0.9,-3.68 -6.21,0 -9.32,1.88 -9.32,5.63 0,4.66 3.92,8.22 11.75,10.68 4.74,1.42 12.21,3.12 22.43,5.11 22.3,4.25 37.06,6.38 44.28,6.38 z"
style="fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none"
id="path4167"
inkscape:connector-curvature="0" /><path
d="m 273.4,308.82 c 5.56,0 10.36,-1.74 14.4,-5.21 4.03,-3.47 6.05,-7.83 6.05,-13.09 0,-3.21 -2.99,-6.3 -8.97,-9.29 l -1.29,-0.65 c -3.75,-1.87 -22.3,-20.98 -55.65,-57.31 -3.44,-3.7 -9.37,-10.95 -17.79,-21.74 -7.51,-10.74 -11.26,-17.1 -11.26,-19.08 0,-0.4 1.99,-0.61 5.97,-0.61 4.46,0 12.38,0.94 23.76,2.8 9.75,1.72 17.73,2.58 23.94,2.58 5.48,0 11.37,-0.44 17.67,-1.32 10.16,-1.31 15.24,-3.46 15.24,-6.45 0,-3.76 -2.52,-5.63 -7.56,-5.63 -1.35,0 -3.89,0.25 -7.61,0.75 -3.73,0.5 -5.99,0.75 -6.79,0.75 -5.23,0 -13.01,-0.79 -23.32,-2.36 l -21.53,-3.2 c -1.47,-0.14 -5.24,-1.06 -11.28,-2.75 -5.6,-1.62 -9.52,-2.43 -11.76,-2.43 -3.84,0 -6.87,1.75 -9.09,5.27 -1.84,2.3 -2.76,5.28 -2.76,8.95 0,4.24 4.34,12.91 13.01,26 8.29,12.28 15.76,21.88 22.44,28.79 l 33.38,38.67 c 11.4,13.07 17.1,20.16 17.1,21.26 0,0.41 -1.85,0.61 -5.54,0.61 -18.25,0 -36.31,-2.5 -54.17,-7.48 -2.48,-0.77 -4.09,-1.42 -4.83,-1.95 -0.74,-0.53 -1.04,-1.75 -0.9,-3.68 -6.21,0 -9.32,1.88 -9.32,5.63 0,4.66 3.92,8.22 11.75,10.68 4.74,1.42 12.21,3.12 22.43,5.11 22.3,4.25 37.06,6.38 44.28,6.38 z"
style="fill:none;stroke:#000000;stroke-width:0.14;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
id="path4169"
inkscape:connector-curvature="0" /><path
d="m 332.58,285.49 c -7.99,0 -15.44,-9.85 -22.35,-29.56 -4.67,-15.36 -7,-29.45 -7,-42.25 0,-24.27 7.15,-36.41 21.45,-36.41 20.26,0 30.39,22.97 30.39,68.9 0,10.03 -1.46,18.56 -4.36,25.61 -3.83,9.14 -9.87,13.71 -18.13,13.71 z m 115.06,5.22 c 5.48,0 9.3,-0.42 11.45,-1.26 4.88,-1.53 7.32,-4.67 7.32,-9.43 0,-4.6 -1.54,-6.9 -4.63,-6.9 -1.53,1.67 -3.53,3.36 -6,5.08 -1.61,0.85 -4.03,1.28 -7.28,1.28 -12.31,0 -24.47,-1.71 -36.49,-5.11 -16.44,-4.68 -24.66,-11.22 -24.66,-19.61 0,-3.33 2.12,-5.84 6.35,-7.53 4.77,-1.61 9.57,-2.41 14.41,-2.41 1.73,0 7.01,0.33 15.85,1.01 8.83,0.58 14.03,0.79 15.58,0.63 2.22,-1.53 3.25,-3.84 3.12,-6.9 -0.3,-3.11 -1.85,-5.1 -4.68,-5.98 -2.84,1.61 -4.78,2.41 -5.8,2.41 -9.72,0 -20.67,-2.48 -32.86,-7.45 -15.89,-6.57 -23.83,-15.08 -23.83,-25.51 0,-8.49 4.86,-14.66 14.59,-18.53 9.48,-3.22 19.55,-4.82 30.23,-4.82 4.25,0 9.71,0.56 16.37,1.69 7.61,1.23 12.77,2.55 15.47,3.96 5.4,2.86 8.7,4.67 9.9,5.44 l 0,5.58 c 6.34,0 9.52,-1.73 9.52,-5.18 0,-6.72 -7.52,-12.44 -22.55,-17.17 -11.46,-3.59 -21.26,-5.38 -29.39,-5.38 -12.92,0 -24.18,2.4 -33.79,7.21 -12.45,6.28 -18.67,15.57 -18.67,27.88 0,8.43 3.33,15.8 9.99,22.1 4.4,4.18 10.87,8.03 19.4,11.54 -5.08,1.18 -8.9,2.91 -11.46,5.17 -4.04,3.74 -6.07,8.51 -6.07,14.3 0,12.3 9.22,21.5 27.65,27.6 12.69,4.2 26.34,6.29 40.96,6.29 z m -124.85,3.95 c 0.8,0 2.07,-0.4 3.78,-1.2 0.68,0.46 1.39,0.69 2.14,0.69 24.14,-1.58 36.22,-17.86 36.22,-48.84 0,-18.27 -2.16,-33.88 -6.47,-46.81 -6.68,-19.92 -17.54,-29.88 -32.58,-29.88 -11.53,0 -20.5,4.84 -26.91,14.51 -5.57,8.42 -8.36,18.48 -8.36,30.21 0,11.36 1.41,23.32 4.21,35.88 2.98,13 7.27,23.4 12.89,31.19 2.57,3.74 4.48,6.47 5.74,8.19 3.23,4.04 6.35,6.06 9.34,6.06 z"
style="fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none"
id="path4171"
inkscape:connector-curvature="0" /><path
d="m 332.58,285.49 c -7.99,0 -15.44,-9.85 -22.35,-29.56 -4.67,-15.36 -7,-29.45 -7,-42.25 0,-24.27 7.15,-36.41 21.45,-36.41 20.26,0 30.39,22.97 30.39,68.9 0,10.03 -1.46,18.56 -4.36,25.61 -3.83,9.14 -9.87,13.71 -18.13,13.71 z m 115.06,5.22 c 5.48,0 9.3,-0.42 11.45,-1.26 4.88,-1.53 7.32,-4.67 7.32,-9.43 0,-4.6 -1.54,-6.9 -4.63,-6.9 -1.53,1.67 -3.53,3.36 -6,5.08 -1.61,0.85 -4.03,1.28 -7.28,1.28 -12.31,0 -24.47,-1.71 -36.49,-5.11 -16.44,-4.68 -24.66,-11.22 -24.66,-19.61 0,-3.33 2.12,-5.84 6.35,-7.53 4.77,-1.61 9.57,-2.41 14.41,-2.41 1.73,0 7.01,0.33 15.85,1.01 8.83,0.58 14.03,0.79 15.58,0.63 2.22,-1.53 3.25,-3.84 3.12,-6.9 -0.3,-3.11 -1.85,-5.1 -4.68,-5.98 -2.84,1.61 -4.78,2.41 -5.8,2.41 -9.72,0 -20.67,-2.48 -32.86,-7.45 -15.89,-6.57 -23.83,-15.08 -23.83,-25.51 0,-8.49 4.86,-14.66 14.59,-18.53 9.48,-3.22 19.55,-4.82 30.23,-4.82 4.25,0 9.71,0.56 16.37,1.69 7.61,1.23 12.77,2.55 15.47,3.96 5.4,2.86 8.7,4.67 9.9,5.44 l 0,5.58 c 6.34,0 9.52,-1.73 9.52,-5.18 0,-6.72 -7.52,-12.44 -22.55,-17.17 -11.46,-3.59 -21.26,-5.38 -29.39,-5.38 -12.92,0 -24.18,2.4 -33.79,7.21 -12.45,6.28 -18.67,15.57 -18.67,27.88 0,8.43 3.33,15.8 9.99,22.1 4.4,4.18 10.87,8.03 19.4,11.54 -5.08,1.18 -8.9,2.91 -11.46,5.17 -4.04,3.74 -6.07,8.51 -6.07,14.3 0,12.3 9.22,21.5 27.65,27.6 12.69,4.2 26.34,6.29 40.96,6.29 z m -124.85,3.95 c 0.8,0 2.07,-0.4 3.78,-1.2 0.68,0.46 1.39,0.69 2.14,0.69 24.14,-1.58 36.22,-17.86 36.22,-48.84 0,-18.27 -2.16,-33.88 -6.47,-46.81 -6.68,-19.92 -17.54,-29.88 -32.58,-29.88 -11.53,0 -20.5,4.84 -26.91,14.51 -5.57,8.42 -8.36,18.48 -8.36,30.21 0,11.36 1.41,23.32 4.21,35.88 2.98,13 7.27,23.4 12.89,31.19 2.57,3.74 4.48,6.47 5.74,8.19 3.23,4.04 6.35,6.06 9.34,6.06 z"
style="fill:none;stroke:#000000;stroke-width:0.14;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
id="path4173"
inkscape:connector-curvature="0" /><g
id="g4175"><g
id="g4177"
clip-path="url(#clipPath4179)"><g
id="g4183"
transform="matrix(41.4,0,0,19.68,310.92,314.4)"><image
width="1"
height="1"
preserveAspectRatio="none"
transform="matrix(1,0,0,-1,0,1)"
xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAHMAAAA3CAYAAAA/inWcAAAABHNCSVQICAgIfAhkiAAAAJpJREFUeJzt0QEJACAQwMDX/p21hCCMuwSDrZk5Q8L+HcA7ZoaYGWJmiJkhZoaYGWJmiJkhZoaYGWJmiJkhZoaYGWJmiJkhZoaYGWJmiJkhZoaYGWJmiJkhZoaYGWJmiJkhZoaYGWJmiJkhZoaYGWJmiJkhZoaYGWJmiJkhZoaYGWJmiJkhZoaYGWJmiJkhZoaYGWJmiJkhZoZcVWkBbaTUQzAAAAAASUVORK5CYII="
mask="url(#mask4185)"
id="image4189" /></g></g></g><path
d="m 329.1,327.95 0,4.21"
style="fill:none;stroke:#000000;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
id="path4191"
inkscape:connector-curvature="0" /><path
d="m 324.1,327.15 4.92,-0.04"
style="fill:none;stroke:#000000;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
id="path4193"
inkscape:connector-curvature="0" /><text
transform="matrix(1,0,0,-1,221.78,133.03)"
style="font-variant:normal;font-weight:normal;font-size:15.96000004px;font-family:'Times New Roman';-inkscape-font-specification:Times-Roman;writing-mode:lr-tb;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none"
id="text4195"><tspan
x="0 13.18296 27.2118 35.000278 45.789242 57.7752 64.11132 75.490799 89.519638 102.7026 108.2088 122.23764 136.26648 150.29532 160.9566 174.98544 187.2108 194.99928 208.18224"
y="0"
sodipodi:role="line"
id="tspan4197">BUILT FOR ANALYTICS</tspan></text>
</g></svg>
\ No newline at end of file
//! moment-timezone.js
//! version : 0.5.13
//! Copyright (c) JS Foundation and other contributors
//! license : MIT
//! github.com/moment/moment-timezone
!function(a,b){"use strict";"function"==typeof define&&define.amd?define(["moment"],b):"object"==typeof module&&module.exports?module.exports=b(require("moment")):b(a.moment)}(this,function(a){"use strict";function b(a){return a>96?a-87:a>64?a-29:a-48}function c(a){var c,d=0,e=a.split("."),f=e[0],g=e[1]||"",h=1,i=0,j=1;for(45===a.