# -*- coding: utf-8 -*- import datetime import werkzeug import logging import functools import werkzeug.utils import werkzeug.urls from collections import defaultdict, OrderedDict from werkzeug.exceptions import NotFound, Forbidden from odoo.addons.website.controllers.main import QueryURL from odoo.http import Controller, Response, request, route as o_route from odoo.osv import expression _logger = logging.getLogger(__name__) def route(routes, **kw): def decorator(f): @o_route(routes, **kw) @functools.wraps(f) def response_wrap(*args, **kwargs): projects = request.env['runbot.project'].search([('hidden', '=', False)]) more = request.httprequest.cookies.get('more', False) == '1' filter_mode = request.httprequest.cookies.get('filter_mode', 'all') keep_search = request.httprequest.cookies.get('keep_search', False) == '1' cookie_search = request.httprequest.cookies.get('search', '') refresh = kwargs.get('refresh', False) nb_build_errors = request.env['runbot.build.error'].search_count([]) nb_assigned_errors = request.env['runbot.build.error'].search_count([('responsible', '=', request.env.user.id)]) nb_team_errors = request.env['runbot.build.error'].search_count([('responsible', '=', False), ('team_id', 'in', request.env.user.runbot_team_ids.ids)]) kwargs['more'] = more kwargs['projects'] = projects response = f(*args, **kwargs) if isinstance(response, Response): if keep_search and cookie_search and 'search' not in kwargs: search = cookie_search else: search = kwargs.get('search', '') has_pr = kwargs.get('has_pr', None) if keep_search and cookie_search != search: response.set_cookie('search', search) project = response.qcontext.get('project') or projects and projects[0] response.qcontext['theme'] = kwargs.get('theme', request.httprequest.cookies.get('theme', 'legacy')) response.qcontext['projects'] = projects response.qcontext['more'] = more response.qcontext['keep_search'] = keep_search response.qcontext['search'] = search response.qcontext['current_path'] = request.httprequest.full_path response.qcontext['refresh'] = refresh response.qcontext['filter_mode'] = filter_mode response.qcontext['default_category'] = request.env['ir.model.data']._xmlid_to_res_id('runbot.default_category') slug = request.env['ir.http']._slug response.qcontext['qu'] = QueryURL('/runbot/%s' % (slug(project) if project else ''), search=search, refresh=refresh, has_pr=has_pr) if 'title' not in response.qcontext: response.qcontext['title'] = 'Runbot %s' % project.name or '' response.qcontext['nb_build_errors'] = nb_build_errors response.qcontext['nb_assigned_errors'] = nb_assigned_errors response.qcontext['nb_team_errors'] = nb_team_errors if 'page_info_state' not in response.qcontext: response.qcontext['page_info_state'] = 'ok' return response return response_wrap return decorator class Runbot(Controller): def _pending(self): ICP = request.env['ir.config_parameter'].sudo().get_param warn = int(ICP('runbot.pending.warning', 5)) crit = int(ICP('runbot.pending.critical', 12)) pending_count = request.env['runbot.build'].search_count([('local_state', '=', 'pending'), ('build_type', '!=', 'scheduled'), ('host', '=', False)]) scheduled_count = request.env['runbot.build'].search_count([('local_state', '=', 'pending'), ('build_type', '=', 'scheduled')]) pending_assigned_count = request.env['runbot.build'].search_count([('local_state', '=', 'pending'), ('build_type', '!=', 'scheduled'), ('host', '!=', False)]) level = ['info', 'warning', 'danger'][int(pending_count > warn) + int(pending_count > crit)] return pending_count, level, scheduled_count, pending_assigned_count @o_route([ '/runbot/submit' ], type='http', auth="public", methods=['GET', 'POST'], csrf=False) def submit(self, more=False, redirect='/', keep_search=False, category=False, filter_mode=False, update_triggers=False, **kwargs): assert redirect.startswith('/') response = werkzeug.utils.redirect(redirect) response.set_cookie('more', '1' if more else '0') if update_triggers: enabled_triggers = [] project_id = int(update_triggers) for key in kwargs.keys(): if key.startswith('trigger_'): enabled_triggers.append(key.replace('trigger_', '')) key = 'trigger_display_%s' % project_id if len(request.env['runbot.trigger'].search([('project_id', '=', project_id)])) == len(enabled_triggers): response.delete_cookie(key) else: response.set_cookie(key, '-'.join(enabled_triggers)) return response @route(['/', '/runbot', '/runbot/', '/runbot//search/'], website=True, auth='public', type='http') def bundles(self, project=None, search='', projects=False, refresh=False, for_next_freeze=False, limit=40, has_pr=None, **kwargs): search = search if len(search) < 60 else search[:60] env = request.env categories = env['runbot.category'].search([]) if not project and projects: project = projects[0] pending_count, level, scheduled_count, pending_assigned_count = self._pending() context = { 'categories': categories, 'search': search, 'message': request.env['ir.config_parameter'].sudo().get_param('runbot.runbot_message'), 'pending_count': pending_count, 'pending_assigned_count': pending_assigned_count, 'pending_level': level, 'scheduled_count': scheduled_count, 'hosts_data': request.env['runbot.host'].search([('assigned_only', '=', False)]), } if project: domain = [('last_batch', '!=', False), ('project_id', '=', project.id)] if not search: domain.append(('no_build', '=', False)) if has_pr is not None: domain.append(('has_pr', '=', bool(has_pr))) filter_mode = request.httprequest.cookies.get('filter_mode', False) if filter_mode == 'sticky': domain.append(('sticky', '=', True)) elif filter_mode == 'nosticky': domain.append(('sticky', '=', False)) if for_next_freeze: domain.append(('for_next_freeze', '=', True)) if search: search_domains = [] pr_numbers = [] for search_elem in search.split("|"): if search_elem.isnumeric(): pr_numbers.append(int(search_elem)) operator = '=ilike' if '%' in search_elem else 'ilike' search_domains.append([('name', operator, search_elem)]) if pr_numbers: res = request.env['runbot.branch'].search([('name', 'in', pr_numbers)]) if res: search_domains.append([('id', 'in', res.mapped('bundle_id').ids)]) search_domain = expression.OR(search_domains) domain = expression.AND([domain, search_domain]) e = expression.expression(domain, request.env['runbot.bundle']) query = e.query query.order = """ (case when "runbot_bundle".sticky then 1 when "runbot_bundle".sticky is null then 2 else 2 end), case when "runbot_bundle".sticky then "runbot_bundle".version_number end collate "C" desc, "runbot_bundle".last_batch desc """ query.limit = min(int(limit), 200) bundles = env['runbot.bundle'].browse(query) category_id = int(request.httprequest.cookies.get('category') or 0) or request.env['ir.model.data']._xmlid_to_res_id('runbot.default_category') trigger_display = request.httprequest.cookies.get('trigger_display_%s' % project.id, None) if trigger_display is not None: trigger_display = [int(td) for td in trigger_display.split('-') if td] bundles = bundles.with_context(category_id=category_id) triggers = env['runbot.trigger'].search([('project_id', '=', project.id)]) context.update({ 'active_category_id': category_id, 'bundles': bundles, 'project': project, 'triggers': triggers, 'trigger_display': trigger_display, 'has_pr': has_pr, 'search': search, }) context.update({'message': request.env['ir.config_parameter'].sudo().get_param('runbot.runbot_message')}) # request.is_frontend = False # remove inherit branding return request.render('runbot.bundles', context) @route([ '/runbot/bundle/', '/runbot/bundle//page/', '/runbot/bundle/', ], website=True, auth='public', type='http', sitemap=False) def bundle(self, bundle=None, page=1, limit=50, **kwargs): if isinstance(bundle, str): bundle = request.env['runbot.bundle'].search([('name', '=', bundle)], limit=1, order='id') if not bundle: raise NotFound slug = request.env['ir.http']._slug return werkzeug.utils.redirect(f'/runbot/bundle/{slug(bundle)}') domain = [('bundle_id', '=', bundle.id), ('hidden', '=', False)] batch_count = request.env['runbot.batch'].search_count(domain) pager = request.website.pager( url='/runbot/bundle/%s' % bundle.id, total=batch_count, page=page, step=50, ) batchs = request.env['runbot.batch'].search(domain, limit=limit, offset=pager.get('offset', 0), order='id desc') context = { 'bundle': bundle, 'batchs': batchs, 'pager': pager, 'project': bundle.project_id, 'title': 'Bundle %s' % bundle.name, 'page_info_state': bundle.last_batch._get_global_result(), } return request.render('runbot.bundle', context) @o_route([ '/runbot/bundle//force', '/runbot/bundle//force/', ], type='http', auth="user", methods=['GET', 'POST'], csrf=False) def force_bundle(self, bundle, auto_rebase=False, **_post): if not request.env.user.has_group('runbot.group_runbot_advanced_user') and ':' not in bundle.name: raise Forbidden("Only users with a specific group can do that. Please contact runbot administrators") _logger.info('user %s forcing bundle %s', request.env.user.name, bundle.name) # user must be able to read bundle batch = bundle.sudo()._force() batch._log('Batch forced by %s', request.env.user.name) batch._prepare(auto_rebase) batch._process() return werkzeug.utils.redirect('/runbot/batch/%s' % batch.id) @route(['/runbot/batch/'], website=True, auth='public', type='http', sitemap=False) def batch(self, batch_id=None, **kwargs): batch = request.env['runbot.batch'].browse(batch_id) context = { 'batch': batch, 'project': batch.bundle_id.project_id, 'title': 'Batch %s (%s)' % (batch.id, batch.bundle_id.name), 'page_info_state': batch._get_global_result(), } return request.render('runbot.batch', context) @o_route(['/runbot/batch/slot//build'], auth='user', type='http') def slot_create_build(self, slot=None, **kwargs): build = slot.sudo()._create_missing_build() return werkzeug.utils.redirect('/runbot/build/%s' % build.id) @route([ '/runbot/commit/', '/runbot/commit/' ], website=True, auth='public', type='http', sitemap=False) def commit(self, commit=None, commit_hash=None, **kwargs): if commit_hash: commit = request.env['runbot.commit'].search([('name', '=like', f'{commit_hash}%')], limit=1) if not commit.exists(): raise NotFound() slug = request.env['ir.http']._slug return request.redirect(f"/runbot/commit/{slug(commit)}") status_list = request.env['runbot.commit.status'].search([('commit_id', '=', commit.id)], order='id desc') last_status_by_context = dict() for status in status_list: if status.context in last_status_by_context: continue last_status_by_context[status.context] = status context = { 'commit': commit, 'project': commit.repo_id.project_id, 'reflogs': request.env['runbot.ref.log'].search([('commit_id', '=', commit.id)]), 'status_list': status_list, 'last_status_by_context': last_status_by_context, 'title': 'Commit %s' % commit.name[:8] } return request.render('runbot.commit', context) @o_route(['/runbot/commit/resend/'], website=True, auth='user', type='http') def resend_status(self, status_id=None, **kwargs): CommitStatus = request.env['runbot.commit.status'] status = CommitStatus.browse(status_id) if not status.exists(): raise NotFound() last_status = CommitStatus.search([('commit_id', '=', status.commit_id.id), ('context', '=', status.context)], order='id desc', limit=1) if status != last_status: raise Forbidden("Only the last status can be resent") if not last_status.sent_date or (datetime.datetime.now() - last_status.sent_date).seconds > 60: # ensure at least 60sec between two resend new_status = status.sudo().copy() new_status.description = 'Status resent by %s' % request.env.user.name new_status._send() _logger.info('github status %s resent by %s', status_id, request.env.user.name) return werkzeug.utils.redirect('/runbot/commit/%s' % status.commit_id.id) @o_route([ '/runbot/build//', ], type='http', auth="user", methods=['POST'], csrf=False) def build_operations(self, build_id, operation, **post): build = request.env['runbot.build'].sudo().browse(build_id) if operation == 'rebuild': build = build._rebuild() elif operation == 'kill': build._ask_kill() elif operation == 'wakeup': build._wake_up() return str(build.id) @route([ '/runbot/build/', '/runbot/batch//build/' ], type='http', auth="public", website=True, sitemap=False) def build(self, build_id, search=None, from_batch=None, **post): """Events/Logs""" if from_batch: from_batch = request.env['runbot.batch'].browse(int(from_batch)) if build_id not in from_batch.with_context(active_test=False).slot_ids.build_id.ids: # the url may have been forged replacing the build id, redirect to hide the batch return werkzeug.utils.redirect('/runbot/build/%s' % build_id) from_batch = from_batch.with_context(batch=from_batch) Build = request.env['runbot.build'].with_context(batch=from_batch) build = Build.browse([build_id])[0] if not build.exists(): return request.not_found() siblings = (build.parent_id.children_ids if build.parent_id else from_batch.slot_ids.build_id if from_batch else build).sorted('id') context = { 'build': build, 'from_batch': from_batch, 'project': build.params_id.trigger_id.project_id, 'title': 'Build %s' % build.id, 'siblings': siblings, 'page_info_state': build.global_result, # following logic is not the most efficient but good enough 'prev_ko': next((b for b in reversed(siblings) if b.id < build.id and b.global_result != 'ok'), Build), 'prev_bu': next((b for b in reversed(siblings) if b.id < build.id), Build), 'next_bu': next((b for b in siblings if b.id > build.id), Build), 'next_ko': next((b for b in siblings if b.id > build.id and b.global_result != 'ok'), Build), } return request.render("runbot.build", context) @route([ '/runbot/build/search', ], website=True, auth='public', type='http', sitemap=False) def builds(self, **kwargs): domain = [] for key in ('config_id', 'version_id', 'project_id', 'trigger_id', 'create_batch_id.bundle_id', 'create_batch_id'): # allowed params value = kwargs.get(key) if value: domain.append((f'params_id.{key}', '=', int(value))) for key in ('global_state', 'local_state', 'global_result', 'local_result'): value = kwargs.get(key) if value: domain.append((f'{key}', '=', value)) for key in ('description',): if key in kwargs: domain.append((f'{key}', 'ilike', kwargs.get(key))) context = { 'builds': request.env['runbot.build'].search(domain, limit=100), } return request.render('runbot.build_search', context) @route([ '/runbot/branch/', ], website=True, auth='public', type='http', sitemap=False) def branch(self, branch=None, **kwargs): pr_branch = branch.bundle_id.branch_ids.filtered(lambda rec: not rec.is_pr and rec.id != branch.id and rec.remote_id.repo_id == branch.remote_id.repo_id)[:1] branch_pr = branch.bundle_id.branch_ids.filtered(lambda rec: rec.is_pr and rec.id != branch.id and rec.remote_id.repo_id == branch.remote_id.repo_id)[:1] context = { 'branch': branch, 'project': branch.remote_id.repo_id.project_id, 'title': 'Branch %s' % branch.name, 'pr_branch': pr_branch, 'branch_pr': branch_pr } return request.render('runbot.branch', context) @route([ '/runbot/glances', '/runbot/glances/' ], type='http', auth='public', website=True, sitemap=False) def glances(self, project_id=None, **kwargs): project_ids = [project_id] if project_id else request.env['runbot.project'].search([]).ids # search for access rights bundles = request.env['runbot.bundle'].search([('sticky', '=', True), ('project_id', 'in', project_ids)]) pending_count, level, scheduled_count, pending_assigned_count = self._pending() qctx = { 'pending_count': pending_count, 'pending_assigned_count': pending_assigned_count, 'pending_level': level, 'bundles': bundles, 'title': 'Glances' } return request.render("runbot.glances", qctx) @route(['/runbot/monitoring', '/runbot/monitoring/', '/runbot/monitoring//'], type='http', auth='user', website=True, sitemap=False) def monitoring(self, category_id=None, view_id=None, **kwargs): pending_count, level, scheduled_count, pending_assigned_count = self._pending() hosts_data = request.env['runbot.host'].search([]) if category_id: category = request.env['runbot.category'].browse(category_id) assert category.exists() else: category = request.env.ref('runbot.nightly_category') category_id = category.id bundles = request.env['runbot.bundle'].search([('sticky', '=', True)]) # NOTE we dont filter on project qctx = { 'category': category, 'pending_count': pending_count, 'pending_assigned_count': pending_assigned_count, 'pending_level': level, 'scheduled_count': scheduled_count, 'bundles': bundles, 'hosts_data': hosts_data, 'auto_tags': request.env['runbot.build.error']._disabling_tags(), 'build_errors': request.env['runbot.build.error'].search([('random', '=', True)]), 'kwargs': kwargs, 'title': 'monitoring' } return request.render(view_id if view_id else "runbot.monitoring", qctx) @route(['/runbot/errors/assign/' ], type='http', auth='user', methods=['POST'], csrf=False, sitemap=False) def build_errors_assign(self, build_error_id=None, **kwargs): build_error = request.env['runbot.build.error'].browse(build_error_id) if build_error.responsible: return build_error.responsible.name if request.env.user._is_internal(): build_error.sudo().responsible = request.env.user return request.env.user.name return 'Error' @route(['/runbot/errors', '/runbot/errors/page/' ], type='http', auth='user', website=True, sitemap=False) def build_errors(self, sort=None, page=1, limit=20, **kwargs): sort_order_choices = { 'last_seen_date desc': 'Last seen date: Newer First', 'last_seen_date asc': 'Last seen date: Older First', 'build_count desc': 'Number seen: High to Low', 'build_count asc': 'Number seen: Low to High', 'responsible asc': 'Assignee: A - Z', 'responsible desc': 'Assignee: Z - A', 'module_name asc': 'Module name: A - Z', 'module_name desc': 'Module name: Z -A' } sort_order = sort if sort in sort_order_choices else 'last_seen_date desc' current_user_errors = request.env['runbot.build.error'].search([ ('responsible', '=', request.env.user.id), ], order='last_seen_date desc, build_count desc') current_team_errors = request.env['runbot.build.error'].search([ ('responsible', '=', False), ('team_id', 'in', request.env.user.runbot_team_ids.ids) ], order='last_seen_date desc, build_count desc') domain = [('responsible', '!=', request.env.user.id), ('build_count', '>', 1)] build_errors_count = request.env['runbot.build.error'].search_count(domain) url_args = {} url_args['sort'] = sort pager = request.website.pager(url='/runbot/errors/', url_args=url_args, total=build_errors_count, page=page, step=limit) build_errors = request.env['runbot.build.error'].search(domain, order=sort_order, limit=limit, offset=pager.get('offset', 0)) qctx = { 'current_user_errors': current_user_errors, 'current_team_errors': current_team_errors, 'build_errors': build_errors, 'title': 'Build Errors', 'sort_order_choices': sort_order_choices, 'page': page, 'pager': pager, } return request.render('runbot.build_error', qctx) @route(['/runbot/teams', '/runbot/teams/',], type='http', auth='user', website=True, sitemap=False) def team_dashboards(self, team=None, hide_empty=False, **kwargs): teams = request.env['runbot.team'].search([]) if not team else None domain = [('id', 'in', team.assignment_ids.ids)] if team else [] # Sort & Filter sortby = kwargs.get('sortby', 'count') searchbar_sortings = { 'date': {'label': 'Recently Seen', 'order': 'last_seen_date desc'}, 'count': {'label': 'Nb Seen', 'order': 'build_count desc'}, } order = searchbar_sortings[sortby]['order'] searchbar_filters = { 'all': {'label': 'All', 'domain': []}, 'unassigned': {'label': 'Unassigned', 'domain': [('responsible', '=', False)]}, 'not_one': {'label': 'Seen more than once', 'domain': [('build_count', '>', 1)]}, } for trigger in team.assignment_ids.trigger_ids if team else []: k = f'trigger_{trigger.name.lower().replace(" ", "_")}' searchbar_filters.update( {k: {'label': f'Trigger {trigger.name}', 'domain': [('trigger_ids', '=', trigger.id)]}} ) filterby = kwargs.get('filterby', 'not_one') if filterby not in searchbar_filters: filterby = 'not_one' domain = expression.AND([domain, searchbar_filters[filterby]['domain']]) qctx = { 'team': team, 'teams': teams, 'build_assignment_ids': request.env['runbot.build.error'].search(domain, order=order), 'hide_empty': bool(hide_empty), 'searchbar_sortings': searchbar_sortings, 'sortby': sortby, 'searchbar_filters': OrderedDict(sorted(searchbar_filters.items())), 'filterby': filterby, 'default_url': request.httprequest.path, } return request.render('runbot.team', qctx) @route(['/runbot/dashboards/',], type='http', auth='user', website=True, sitemap=False) def dashboards(self, dashboard=None, hide_empty=False, **kwargs): qctx = { 'dashboard': dashboard, 'hide_empty': bool(hide_empty), } return request.render('runbot.dashboard_page', qctx) @route(['/runbot/build/stats/'], type='http', auth="public", website=True, sitemap=False) def build_stats(self, build_id, search=None, **post): """Build statistics""" Build = request.env['runbot.build'] build = Build.browse([build_id])[0] if not build.exists(): return request.not_found() build_stats = defaultdict(dict) for stat in build.stat_ids: for module, value in sorted(stat.values.items(), key=lambda item: item[1], reverse=True): build_stats[stat.category][module] = value context = { 'build': build, 'build_stats': build_stats, 'project': build.params_id.trigger_id.project_id, 'title': 'Build %s statistics' % build.id } return request.render("runbot.build_stats", context) @route(['/runbot/stats/'], type='json', auth="public", website=False, sitemap=False) def stats_json(self, bundle_id=False, trigger_id=False, key_category='', center_build_id=False, ok_only=False, limit=100, search=None, **post): """ Json stats """ trigger_id = trigger_id and int(trigger_id) bundle_id = bundle_id and int(bundle_id) center_build_id = center_build_id and int(center_build_id) limit = min(int(limit), 1000) trigger = request.env['runbot.trigger'].browse(trigger_id) bundle = request.env['runbot.bundle'].browse(bundle_id) if not trigger_id or not bundle_id or not trigger.exists() or not bundle.exists(): return request.not_found() builds_domain = [ ('global_state', 'in', ('running', 'done')), ('slot_ids.batch_id.bundle_id', '=', bundle_id), ('params_id.trigger_id', '=', trigger.id), ] if ok_only: builds_domain += ('global_result', '=', 'ok') builds = request.env['runbot.build'].with_context(active_test=False) if center_build_id: builds = builds.search( expression.AND([builds_domain, [('id', '>=', center_build_id)]]), order='id', limit=limit/2) builds_domain = expression.AND([builds_domain, [('id', '<=', center_build_id)]]) limit -= len(builds) builds |= builds.search(builds_domain, order='id desc', limit=limit) if not builds: return {} builds = builds.search([('id', 'child_of', builds.ids)]) parents = {b.id: b.top_parent.id for b in builds.with_context(prefetch_fields=False)} request.env.cr.execute("SELECT build_id, values FROM runbot_build_stat WHERE build_id IN %s AND category = %s", [tuple(builds.ids), key_category]) # read manually is way faster than using orm res = {} for (build_id, values) in request.env.cr.fetchall(): if values: res.setdefault(parents[build_id], {}).update(values) # we need to update here to manage the post install case: we want to combine stats from all post_install childrens. return res @route(['/runbot/stats//'], type='http', auth="public", website=True, sitemap=False) def modules_stats(self, bundle, trigger, search=None, **post): """Modules statistics""" categories = set() def list_config_categories(config): nonlocal categories for config_step in config.step_ids: regex_ids = config_step.build_stat_regex_ids if not regex_ids: regex_ids = regex_ids.search([('generic', '=', True)]) categories |= set(regex_ids.mapped('name')) for config in config_step.create_config_ids: list_config_categories(config) list_config_categories(trigger.config_id) categories = sorted(categories) context = { 'stats_categories': categories, 'bundle': bundle, 'trigger': trigger, } return request.render("runbot.modules_stats", context) @route(['/runbot/load_info'], type='http', auth="user", website=True, sitemap=False) def load_infos(self, **post): build_by_bundle = {} for build in request.env['runbot.build'].search([('local_state', 'in', ('pending', 'testing'))], order='id'): build_by_bundle.setdefault(build.params_id.create_batch_id.bundle_id, []).append(build) build_by_bundle = list(build_by_bundle.items()) build_by_bundle.sort(key=lambda x: -len(x[1])) pending_count, level, scheduled_count, pending_assigned_count = self._pending() context = { 'build_by_bundle': build_by_bundle, 'pending_count': pending_count, 'pending_assigned_count': pending_assigned_count, 'pending_level': level, 'scheduled_count': scheduled_count, 'hosts_data': request.env['runbot.host'].search([('assigned_only', '=', False)]), } return request.render("runbot.load_info", context) @route([ '/runbot/run/', '/runbot/run//', ], type='http', auth="public", website=True, sitemap=False) def access_running(self, build_id, db_suffix=None, **kwargs): build = request.env['runbot.build'].browse(int(build_id)).exists() run_url = build._get_run_url(db_suffix) _logger.info('Redirecting to %s', run_url) return werkzeug.utils.redirect(run_url) @route(['/runbot/parse_log/'], type='http', auth='user', sitemap=False) def parse_log(self, ir_log, **kwargs): request.env['runbot.build.error']._parse_logs(ir_log) return werkzeug.utils.redirect('/runbot/build/%s' % ir_log.build_id.id) @route(['/runbot/bundle/toggle_no_build//'], type='http', auth='user', sitemap=False) def toggle_no_build(self, bundle_id, value, **kwargs): if not request.env.user.has_group('base.group_user'): return 'Forbidden' bundle = request.env['runbot.bundle'].browse(bundle_id).exists() if bundle.sticky or bundle.is_base: return 'Forbidden' if bundle.project_id.tmp_prefix and bundle.name.startswith(bundle.project_id.tmp_prefix): return 'Forbidden' bundle.sudo().no_build = bool(value) _logger.info('Bundle %s no_build set to %s by %s', bundle.name, bool(value), request.env.user.name) return werkzeug.utils.redirect(f'/runbot/bundle/{bundle_id}') @route(['/runbot/trigger/report/'], type='http', auth='user', website=True, sitemap=False) def report_view(self, trigger_id=None, **kwargs): return request.render("runbot.trigger_report", { 'trigger': trigger_id, 'project': trigger_id.project_id, }) @route([ '/runbot/dockerfile', '/runbot/dockerfile/', '/runbot/dockerfile/tag/', '/runbot/dockerfile/version/', ], type='http', auth='public', sitemap=False) def dockerfile_content(self, dockerfile_id=None, version=None, docker_tag=None, **kwargs): dockerfile_sudo = request.env['runbot.dockerfile'].sudo() if 'id' in kwargs: dockerfile_id = int(kwargs['id']) if dockerfile_id: # keep 'id' for historical reasons dockerfile = dockerfile_sudo.browse(dockerfile_id).exists() elif docker_tag: dockerfile = dockerfile_sudo.search([('image_tag', '=', docker_tag)]) elif version: dockerfile = dockerfile_sudo.search([('version_ids.name', '=', version)]) else: raise NotFound if dockerfile.public_visibility: return Response(response=dockerfile.layer_ids.render_layers({ 'USERUID': '${USERUID}', 'USERGID': '${USERGID}', 'USERNAME': '${USERNAME}', }), status=200, mimetype='text/plain') if dockerfile: _logger.error('Trying to access a non public docker image') raise NotFound