Source code for toxicbuild.ui.web

# -*- coding: utf-8 -*-

# Copyright 2015-2017 Juca Crispim <>

# This file is part of toxicbuild.

# toxicbuild is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.

# toxicbuild is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# GNU General Public License for more details.

# You should have received a copy of the GNU General Public License
# along with toxicbuild. If not, see <>.


import asyncio
    from asyncio import ensure_future
except ImportError:  # pragma no cover
    from asyncio import async as ensure_future

import datetime
import traceback
from tornado import gen
from tornado.websocket import WebSocketHandler, WebSocketError
from pyrocumulus.web.applications import (PyroApplication, StaticApplication)
from pyrocumulus.web.handlers import TemplateHandler, PyroRequest
from pyrocumulus.web.urlmappers import URLSpec
from toxicbuild.core.utils import bcrypt_string, LoggerMixin, string2datetime
from toxicbuild.ui import settings
from toxicbuild.ui.connectors import StreamConnector
from toxicbuild.ui.models import Repository, Slave, BuildSet, Builder, Plugin
from toxicbuild.ui.utils import format_datetime, is_datetime

COOKIE_NAME = 'toxicui'

[docs]class LoginHandler(TemplateHandler):
[docs] def get(self, action): if action == 'logout': self.clear_cookie(COOKIE_NAME) return self.redirect('/') if self.get_secure_cookie(COOKIE_NAME): return self.redirect('/') error = bool(self.params.get('error')) self.render_template('login.html', {'error': error})
[docs] def post(self, action): if not self.params.get('username') == settings.USERNAME: return self.redirect('/login?error=1') salt = settings.BCRYPT_SALT passwd = settings.PASSWORD if not bcrypt_string(self.params.get('password'), salt) == passwd: return self.redirect('/login?error=1') self.set_secure_cookie(COOKIE_NAME, 'SAUCIFUFU!') self.redirect('/')
[docs]class LoggedTemplateHandler(TemplateHandler):
[docs] def prepare(self): super().prepare() cookie = self.get_secure_cookie(COOKIE_NAME) if not cookie: self.redirect('/login')
[docs]class BaseModelHandler(LoggedTemplateHandler): item_template = 'item.html' list_template = 'list.html'
[docs] def initialize(self, *args, **kwargs): self.model = kwargs['model'] del kwargs['model'] super().initialize(*args, **kwargs)
[docs] def get_item(self, **kwargs): item = yield from self.model.get(**kwargs) return item
[docs] def add(self, **kwargs): resp = yield from self.model.add(**kwargs) json_resp = resp.to_json() return json_resp
[docs] def get(self, *args): resp = yield from self.get_item(**self.params) json_resp = resp.to_json() return json_resp
[docs] def post(self, *args): resp = yield from self.add(**self.params) self.write(resp)
[docs] def delete(self, *args): item = yield from self.get_item(**self.params) resp = yield from item.delete() return resp
[docs]class RepositoryHandler(BaseModelHandler): @gen.coroutine
[docs] def post(self, *args): if 'add-branch' in args: yield from self.add_branch() return elif 'remove-branch' in args: yield from self.remove_branch() return elif 'enable-plugin' in self.request.uri: yield from self.enable_plugin() return elif 'disable-plugin' in self.request.uri: yield from self.disable_plugin() return elif'start-build' not in args: yield super().post(*args) return ret = yield from self.start_build() self.write(ret)
[docs] def enable_plugin(self): repo = yield from self.get_item(repo_name=self.params.get('name')) del self.params['name'] plugin_name = self.params.get('plugin_name') del self.params['plugin_name'] r = yield from repo.enable_plugin(plugin_name, **self.params) return r
[docs] def disable_plugin(self): repo = yield from self.get_item(repo_name=self.params.get('name')) plugin_name = self.params.get('plugin_name') r = yield from repo.disable_plugin(name=plugin_name) return r
[docs] def list_plugins(self): plugins = yield from Plugin.list() return plugins
[docs] def start_build(self): item = yield from self.get_item(repo_name=self.params.get('name')) del self.params['name'] ret = yield from item.start_build(**self.params) return ret
[docs] def add_branch(self): item = yield from self.get_item(repo_name=self.params.get('name')) del self.params['name'] notify = self.params['notify_only_latest'] notify = True if notify == 'true' else False self.params['notify_only_latest'] = notify r = yield from item.add_branch(**self.params) return r
[docs] def remove_branch(self): item = yield from self.get_item(repo_name=self.params.get('name')) del self.params['name'] r = yield from item.remove_branch(**self.params) return r
[docs] def prepare(self): super().prepare() if 'start-build' in self.request.uri: self._prepare_start_build() elif 'add-branch' in self.request.uri: kw = {'name': self.params.get('name'), 'branch_name': self.params.get('branch_name'), 'notify_only_latest': self.params.get('notify_only_latest')} self.params = kw elif 'remove-branch' in self.request.uri: kw = {'name': self.params.get('name'), 'branch_name': self.params.get('branch_name')} self.params = kw elif ('enable-plugin' in self.request.uri or 'disable-plugin' in self.request.uri): yield from self._prepare_for_plugin() else: kw = {} kw['name'] = self.params.get('name') kw['url'] = self.params.get('url') kw['vcs_type'] = self.params.get('vcs_type') kw['update_seconds'] = self.params.get('update_seconds') kw['parallel_builds'] = self.params.get('parallel_builds') kw['slaves'] = self.params.getlist('slaves') self.params = kw
[docs] def delete(self, *args): self.params = {'repo_name': self.params.get('name')} yield super().delete(*args)
[docs] def put(self, *args): item = yield from self.get_item(repo_name=self.params['name']) del self.params['name'] r = yield from item.update(**self.params) self.write(r)
def _prepare_start_build(self): kw = {} kw['name'] = self.params.get('name') kw['builder_name'] = self.params.get('builder_name') kw['branch'] = self.params.get('branch') kw['slaves'] = self.params.getlist('slaves') kw['named_tree'] = self.params.get('named_tree') self.params = kw @asyncio.coroutine def _prepare_for_plugin(self): kw = {} plugin_name = self.params.get('plugin_name') plugin = yield from Plugin.get(name=plugin_name) for k, v in self.params.items(): try: kw[k] = v[0] if getattr(plugin, k)['type'] != 'list' else [ i.strip() for i in v[0].split(',')] except (AttributeError, TypeError): # TypeError happens when a attribute is not a dict # ie, plugin pretty_name and description kw[k] = v[0] self.params = kw
[docs]class SlaveHandler(BaseModelHandler):
[docs] def prepare(self): super().prepare() kw = {} kw['name'] = self.params.get('name') kw['host'] = self.params.get('host') kw['port'] = self.params.get('port') kw['token'] = self.params.get('token') self.params = kw
[docs] def delete(self, *args): self.params = {'slave_name': self.params.get('name')} yield super().delete(*args)
[docs] def put(self, *args): item = yield from self.get_item(slave_name=self.params['name']) yield from item.update(**self.params)
[docs]class StreamHandler(LoggerMixin, WebSocketHandler):
[docs] def initialize(self): self.action = None self.repo_id = None = {'repo_status_changed': self._send_repo_status_info, 'build_started': self._send_build_info, 'build_finished': self._send_build_info, 'build_added': self._send_build_info, 'step_started': self._send_build_info, 'step_finished': self._send_build_info, 'step_output_info': self._send_step_output_info} # maps actions to message (event) types self.action_messages = {'repo-status': ['repo_status_changed'], 'builds': ['build_started', 'build_finished', 'build_added', 'step_started', 'step_finished'], 'step-output': ['step_output_info']}
def _bad_message_type_logger(self, message): msg = 'Bad. message type: {}'.format(message['event_type']) self.log(msg, level='warning') def _get_repo_id(self): repo_id = None keys = ['repo_id', 'repository_id'] for key in keys: try: repo_id = self.request.arguments.get(key)[0].decode() break except TypeError: pass return repo_id
[docs] def open(self, action): self.action = action self.repo_id = self._get_repo_id() f = ensure_future(StreamConnector.plug(self.repo_id, self.receiver)) return f
[docs] def receiver(self, sender, **message): message_type = message.get('event_type') msg = 'message arrived: {}'.format(message_type) self.log(msg, level='debug') if message_type not in self.action_messages.get(self.action, []): msg = 'leaving receiver' self.log(msg, level='debug') return outfn =, self._bad_message_type_logger) try: outfn(message) except Exception: msg = traceback.format_exc() self.log(msg, level='error')
def _send_step_output_info(self, info): """Sends information about step output to the ws client. :param info: Message sent by the master""" step_uuid = self.request.arguments.get('uuid')[0].decode() uuid = info.get('uuid') if step_uuid == uuid: self.write2sock(info) def _send_build_info(self, info): """Sends information about builds to the ws client. :param info: The message sent by the master""" self._format_info_dt(info) self.write2sock(info) def _format_info_dt(self, info): started = info.get('started') if started and is_datetime(started): info['started'] = format_datetime(string2datetime(started)) finished = info.get('finished') if finished and is_datetime(finished): info['finished'] = format_datetime(string2datetime(finished)) created = info.get('created') if created and is_datetime(created): info['created'] = format_datetime(string2datetime(created)) buildset = info.get('buildset') if buildset: self._format_info_dt(buildset) def _send_repo_status_info(self, info): self.write2sock(info)
[docs] def on_close(self): StreamConnector.unplug(self.repo_id, self.receiver)
[docs] def write2sock(self, body): try: self.write_message(body) except WebSocketError: self.log('WebSocketError', level='debug')
[docs]class MainHandler(LoggedTemplateHandler): main_template = 'main.html' @gen.coroutine
[docs] def get(self): repos = yield from Repository.list() slaves = yield from Slave.list() plugins = yield from Plugin.list() context = {'repos': repos, 'slaves': slaves, 'get_btn_class': self._get_btn_class, 'plugins': plugins} self.render_template(self.main_template, context)
def _get_btn_class(self, status): return {'success': 'success', 'fail': 'danger', 'running': 'info', 'exception': 'exception', 'clone-exception': 'exception', 'ready': 'success', 'warning': 'warning', 'cloning': 'pending'}.get(status)
[docs]class WaterfallHandler(LoggedTemplateHandler): template = 'waterfall.html'
[docs] def prepare(self): super().prepare() self.params = PyroRequest(self.request.arguments)
[docs] def get(self, repo_name): buildsets = yield from BuildSet.list(repo_name=repo_name) builders = yield from self._get_builders_for_buildsets(buildsets) repo = yield from Repository.get(repo_name=repo_name) def _ordered_builds(builds): return sorted( builds, key=lambda b: builders[builders.index(b.builder)].name) def fmtdt(dt): # pragma: no cover # when the attribute is not set, it is a empty string int the # template, so we simply skip it here if not isinstance(dt, datetime.datetime): return return format_datetime(dt) context = {'buildsets': buildsets, 'builders': builders, 'ordered_builds': _ordered_builds, 'get_ending': self._get_ending, 'repository': repo, 'fmtdt': fmtdt} self.render_template(self.template, context)
@asyncio.coroutine def _get_builders_for_buildsets(self, buildsets): builders = set() buildsets = buildsets or [] for buildset in buildsets: for build in buildset.builds: builders.add(build.builder) # Now the thing here is: the builders here are made # from the response of buildset-list. It returns only # the builder id for builds, so now I retrieve the # 'full' builder using builder-list ids = [ for b in builders] builders = yield from Builder.list(id__in=ids) builders_dict = { b for b in builders} for buildset in buildsets: for build in buildset.builds: build.builder = builders_dict[] return sorted(builders, key=lambda b: def _get_ending(self, build, build_index, builders): i = build_index while build.builder != builders[i] and len(builders) > i: tag = '</td><td class="builder-column builder-column-id-{}' tag += 'builder-column-index-{}">' yield tag.format(builders[i].id, i + 1) i += 1 yield ''
url = URLSpec('/$', MainHandler) waterfall = URLSpec('/waterfall/(.*)', WaterfallHandler) websocket = URLSpec('/api/socks/(.*)', StreamHandler) login = URLSpec('/(login|logout)', LoginHandler) app = PyroApplication([url, waterfall, login, websocket]) static_app = StaticApplication() repo_kwargs = {'model': Repository} repo_api_url = URLSpec('/api/repo/(.*)', RepositoryHandler, repo_kwargs) slave_kwargs = {'model': Slave} slave_api_url = URLSpec('/api/slave/(.*)', SlaveHandler, slave_kwargs) api_app = PyroApplication([repo_api_url, slave_api_url])